deploro.com Open dashboardDashboard

API Reference

Base URL https://api.deploro.com. Every request except the ones marked public needs a session cookie or a Authorization: Bearer <token> header (see Authentication). Routes marked admin require an admin-role account or an admin-scoped PAT.

Platform auth

Custom PBKDF2 + JWT auth (no external auth provider). Sessions are stateless JWTs in an httpOnly deploro_session cookie, 7-day expiry.

MethodPathAuthDescription
POST/api/auth/setuppublicOne-time admin bootstrap: blocked once any user exists
POST/api/auth/loginpublicSign in with email + password → sets session cookie
POST/api/auth/logoutpublicClears the session cookie
GET/api/auth/sessionpublicCurrent user from the JWT, or null
POST/api/auth/request-otppublicEmail a 6-digit passwordless sign-in code (existing users only)
POST/api/auth/verify-otppublicVerify the code → session cookie, marks email verified
GET/api/auth/oauth/statuspublicWhich platform OAuth providers (google/apple/linkedin) are configured
GET/api/auth/oauth/:provider/startpublicReturns the provider's consent-screen URL
GET/POST/api/auth/oauth/:provider/callbackpublicExchanges the code and signs in, but only if the email already has an account
GET/api/auth/usersadminList platform users
DELETE/api/auth/users/:idadminDelete a user
PATCH/api/auth/users/:id/roleadminSet a user's role
PATCH/api/auth/users/:id/banadminBan/unban: takes effect immediately, no session revocation needed
POST/api/auth/inviteadminCreate an invite → returns acceptUrl
POST/api/auth/accept-invitepublicAccept an invite token, create an account
GET/api/auth/sessionsadminList sessions (always [], since sessions are stateless JWTs)
DELETE/api/auth/sessions/:idadminRevoke a session (no-op for the same reason)
POST/api/auth/loginpublic

Signs in and sets the deploro_session cookie. Use credentials: "include" from a browser.

curl -X POST https://api.deploro.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"..."}'
PATCH/api/auth/users/:id/banadmin

Body: { banned, reason?, expiresAt? }. auth-guard checks banned/banExpires on every request, so a ban blocks the next request immediately.

curl -X PATCH https://api.deploro.com/api/auth/users/USER_ID/ban \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"banned":true,"reason":"abuse","days":7}'

Projects

A project bundles a database, a deploy target, storage, and team access. Creating one does not create its database; that's a separate provisioning step.

MethodPathAuthDescription
GET/api/projectsanyList your projects
POST/api/projectsanyCreate a project (no database until provisioned)
GET/api/projects/:idanyProject details
PATCH/api/projects/:idadminUpdate project settings
DELETE/api/projects/:idadminDelete a project and its database
GET/api/projects/:id/membersanyList members
POST/api/projects/:id/inviteadminInvite a member by email
DELETE/api/projects/:id/members/:uidadminRemove a member
POST/api/projectsany

Body fields (Zod-validated): name (1-50 chars), slug (2-30 chars, lowercase alphanumeric + hyphens), optional git_repo_url, git_branch.

git_repo_url is rejected if it contains embedded credentials (https://user:token@github.com/...). Connect GitHub through OAuth for private repos instead.
curl -X POST https://api.deploro.com/api/projects \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"My App","slug":"my-app"}'
POST/api/projects/:id/databaseany

Provisions the project's isolated Postgres database, idempotently. Until this runs, every /api/db/* route for the project returns 409 {"code":"NO_DATABASE"}.

curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/database \
  -H "Authorization: Bearer deploro_pat_..."

Database

Each project's tables live in its own database's public schema. All traffic to project databases is proxied through gallium-sql-proxy (Workers can't open raw TCP connections).

MethodPathAuthDescription
GET/api/db/tablesanyList project tables
POST/api/db/tablesadminCreate table (structured column defs → CREATE TABLE)
PATCH/api/db/tables/:nameadminRename table
GET/api/db/tables/:nameanyPaginated rows
DELETE/api/db/tables/:nameadminDrop table
GET/api/db/schema/:tableanyColumn info
POST/api/db/queryadminRaw SQL: read-only SELECT only, DDL blocked, audit logged
POST/api/db/rows/:tableanyInsert row
PATCH/api/db/rows/:table/:idanyUpdate row
DELETE/api/db/rows/:table/:idanyDelete row
These routes act on the active project resolved from your session or token; they are not prefixed with /api/projects/:id.
POST/api/db/queryadmin

Body: { sql: string, params?: (string | number | boolean | null)[] }. One read-only statement starting with SELECT, WITH, VALUES or TABLE; it runs in a read-only transaction, so writes and DDL fail. Use migrations for schema changes.

Pass values as params and refer to them as $1, $2, and so on, rather than writing them into the SQL. Quoted values and comments never trip the check, so a search for '%do%' works. Functions that reach other sessions or run SQL held in a string (pg_terminate_backend, pg_stat_activity, set_config, query_to_xml, advisory locks) and U&"..." identifiers are refused. A rejected statement returns 400 with the reason in detail.

curl -X POST https://api.deploro.com/api/db/query \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "x-project-id: <project id>" \
  -H "Content-Type: application/json" \
  -d '{"sql":"select id, full_name from student where full_name ilike $1 limit 10","params":["%do%"]}'
POST/api/db/rows/:tableany

Body: { data: { ...columns } }.

curl -X POST https://api.deploro.com/api/db/rows/todos \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"data":{"title":"Ship the docs site","done":false}}'

Migrations

MethodPathAuthDescription
GET/api/projects/:id/migrationsanyList migrations
POST/api/projects/:id/migrationsanyCreate a migration (name, sql_up, optional sql_down)
POST/api/projects/:id/migrations/:mid/applyanyApply: runs sql_up in a transaction
POST/api/projects/:id/migrations/:mid/rollbackanyRollback: runs sql_down
DELETE/api/projects/:id/migrations/:midanyDelete an unapplied migration
POST/api/projects/:id/migrationsany
curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/migrations \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "add_todos_table",
    "sql_up": "create table todos (id serial primary key, title text not null, done boolean default false);",
    "sql_down": "drop table todos;"
  }'

Storage

R2 object storage, namespaced per project as {slug}/{filename} in a shared bucket. This is where a project's uploaded files belong, not in a bytea column and not on the VPS filesystem, neither of which the dashboard's Storage tab can see.

Two ways to read a file. /storage/download is authenticated and replies with Content-Disposition: attachment, which is right for documents, exports, and anything only project members should see. GET /files/{slug}/{key} is unauthenticated and serves inline, so it can be used directly as an <img src>, but only when a signed URL, a bucket's own public flag, or the project-wide flag says it's allowed (checked in that order). Everything is private by default.

Buckets are named groupings with their own public/private flag, finer-grained than the single project-wide toggle. Upload with a bucket field to store at {slug}/{bucket}/{key} instead of the project root; existing root files are unaffected either way. Signed URLs grant temporary access to one object regardless of its bucket's or the project's own flag, which is useful for sharing a private file without changing any policy.

MethodPathAuthDescription
GET/api/projects/:id/storageanyList R2 objects
POST/api/projects/:id/storage/uploadadminUpload a file: 100MB max, multipart/form-data, optional bucket field
GET/api/projects/:id/storage/download?key=...anyDownload a file (key as query param)
DELETE/api/projects/:id/storage?key=...adminDelete a file (key as query param)
GET/api/projects/:id/storage/bucketsanyList buckets
POST/api/projects/:id/storage/bucketsadminCreate a bucket {name, public?}
PATCH/api/projects/:id/storage/buckets/:nameadminToggle {public}
DELETE/api/projects/:id/storage/buckets/:nameadminRemove the policy (keeps files)
POST/api/projects/:id/storage/signed-urlany{key, expiresIn?}: time-limited public URL (max 7d, default 1h)
GET/files/:slug/:keypublic

Unauthenticated inline read of one object, granted one of three ways, checked in order: a valid ?sig=&exp= signed-URL token (bypasses everything else); the key's first path segment matching a registered bucket, governed by that bucket's own public flag; or, failing both, the project-wide public_storage_enabled flag. Anything else answers 404, indistinguishable from a project that does not exist. Supports HEAD, ETag/If-None-Match revalidation, and Range requests. There is no public listing form; objects are reachable by exact key only.

# turn on the project-wide root flag (project admin)
curl -X PATCH https://api.deploro.com/api/projects/PROJECT_ID   -H "Authorization: Bearer $DEPLORO_TOKEN"   -H "Content-Type: application/json"   -d '{"public_storage_enabled": true}'

# then anyone can read a stored object inline
<img src="https://api.deploro.com/files/my-project/vehicle-images/4/11.png">
POST/api/projects/:id/storage/uploadadmin
curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/storage/upload \
  -H "Authorization: Bearer deploro_pat_..." \
  -F "file=@./avatar.png" \
  -F "bucket=avatars"
POST/api/projects/:id/storage/bucketsadmin
curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/storage/buckets \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "avatars", "public": true}'
POST/api/projects/:id/storage/signed-urlany

HMAC-signed and stateless. There's no token table, so nothing can be revoked early short of rotating the platform's own signing secret. Works on a private root file or a private bucket's file alike.

curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/storage/signed-url \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"key": "invoices/2026-03.pdf", "expiresIn": 3600}'
# => { "url": "https://api.deploro.com/files/my-project/invoices/2026-03.pdf?exp=...&sig=...", "expiresAt": "..." }

Deployments

MethodPathAuthDescription
GET/api/projects/:id/deploymentsanyDeployment history (Worker/Pages only)
POST/api/projects/:id/deploymentsanyTrigger a manual deploy
GET/api/projects/:id/deployments/:didanyGet a deployment + logs (resolves Worker/Pages or VPS)
GET/api/projects/:id/deployments/allanyUnified Worker/Pages + VPS timeline, target-tagged

deployments (Worker/Pages) and vps_deployments (VPS compute, see VPS compute) are separate tables. /deployments/all unions both into one chronological, target-tagged list. Use it for a general "what's deployed" view instead of picking one table.

GitHub integration

Per-user OAuth connection + per-project repo link. Auto-deploys on push once linked.

MethodPathAuthDescription
POST/webhooks/githubpublicInbound GitHub push handler: verified against the project's own webhook secret
GET/api/projects/:id/github/webhookanyThis project's webhook URL + secret (generated lazily), plus auto_deploy_enabled
POST/api/projects/:id/github/webhook/rotateanyIssue a new secret, invalidating the old one immediately
OAuth is the primary connect path. Settings → "Connect GitHub" in the dashboard drives a full GitHub App/OAuth flow (repo picker, branch selector, auto-created webhook) rather than pasting a repo URL. Toggle auto-deploy per project with PATCH /api/projects/:id body {"auto_deploy_enabled": false}.

Domains

Self-service custom domains via Cloudflare SSL for SaaS. Customer DNS CNAMEs to sites.deploro.online, which proxies by hostname to the project's deployment.

MethodPathAuthDescription
GET/api/projects/:id/domainsanyList custom domains
POST/api/projects/:id/domainsanyAdd a domain → returns CNAME + TXT instructions
POST/api/projects/:id/domains/:did/verifyanyVerify TXT → registers CF custom hostname (auto DV SSL)
GET/api/projects/:id/domains/:did/statusanyPoll SSL status (self-heals failed registration)
DELETE/api/projects/:id/domains/:didanyRemove a domain
POST/api/projects/:id/provisional-domainany(Re)provision the {slug}.deploro.app subdomain, idempotent
POST/api/projects/:id/domainsany

Body: { hostname: "app.example.com" }. Returns the CNAME (sites.deploro.online) and TXT verification record to create at your DNS provider, then poll /status or call /verify once they propagate.

curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/domains \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"hostname":"app.example.com"}'

Realtime

MethodPathAuthDescription
GET/api/projects/:id/realtimeanyWebSocket upgrade, one Durable Object per project:table, cookie-authed

Row mutations through /api/db/rows/* publish to connected clients on this channel automatically, with no separate publish call.

Webhooks

Outbound event webhooks, HMAC-signed, fired on data/deploy events for a project.

MethodPathAuthDescription
GET/api/projects/:id/webhooksanyList webhooks
POST/api/projects/:id/webhooksanyCreate a webhook
PATCH/api/projects/:id/webhooks/:widanyUpdate a webhook
DELETE/api/projects/:id/webhooks/:widanyDelete a webhook
POST/api/projects/:id/webhooks/:wid/testanyFire a test payload
GET/api/projects/:id/webhooks/:wid/deliveriesanyLast 30 delivery records
POST/api/projects/:id/webhooksany

Body: { url, events: string[], secret? }. url must be a public HTTP(S) URL with no embedded credentials. secret, if given, is 16-256 chars and used to HMAC-sign deliveries.

curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/webhooks \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/deploro","events":["row.insert","deploy.success"]}'

API Studio

Every table in your project database gets a REST API automatically, plus a generated OpenAPI 3.1 spec, with no code to write.

MethodPathAuthDescription
GET/api/projects/:id/studio/specanyOpenAPI 3.1 spec generated from the current schema
GET/api/projects/:id/studio/:tableanyList rows (filterable via query params)
GET/api/projects/:id/studio/:table/:idanyGet one row by primary key
POST/api/projects/:id/studio/:tableanyInsert a row
PATCH/api/projects/:id/studio/:table/:idanyUpdate a row
DELETE/api/projects/:id/studio/:table/:idanyDelete a row
/api/db/* vs. /api/projects/:id/studio/*: both read/write the same tables. /api/db/* operates on your session's active project; the Studio routes are project-scoped by :id in the path and are what the generated OpenAPI spec describes. Prefer Studio routes for external integrations that reference a specific project.
Write errors. A write that breaks a constraint returns 409 with the Postgres error code and the constraint's name, for example {"error":"duplicate key value violates unique constraint \"seats_email_key\"","code":"23505","constraint":"seats_email_key","table":"seats"}. Use the constraint name to show your own message. Codes: 23505 unique, 23503 foreign key, 23514 check, 23P01 exclusion. A missing required value (23502) or a value of the wrong type (class 22) returns 400 in the same shape. The offending row's values are never included. The same applies to the RPC route and to /data/:slug/*.

Observability

MethodPathAuthDescription
GET/api/projects/:id/observability/auditanyPaginated SQL audit log: every query run against /api/db/query

Hosting

MethodPathAuthDescription
GET/api/hosting/statusanyWorker info + VPS health (admins also get untrackedContainers)
GET/api/hosting/metricsanyLast 60 VPS metric snapshots (cpu/mem/disk)
GET/api/hosting/envanyList Worker secret key names: values are never returned
POST/api/hosting/envanySet a Worker secret via the Cloudflare API

VPS compute hosting

For workloads Workers can't run: a real Postgres connection pool, a real Redis connection for BullMQ, or a headless-Chromium process. Provisions raw Postgres/Redis containers and runs a project's own docker-compose stack on the shared VPS.

Closed to new projects. Every route below (except the read-only status/logs/list endpoints) requires the project to be on a small, explicitly approved allowlist. Every other project gets 403 before anything is provisioned, deployed, or otherwise written to the VPS. There's no API call or dashboard action that adds a project to that list; it's a manual, individually-justified exception. Every other Deploro project is fully managed through the Worker/Pages deploy pipeline and the proxy-managed db; nothing about it ever touches the VPS directly.
MethodPathAuthDescription
POST/api/projects/:id/vps/databaseanyProvision raw Postgres: idempotent; connection string shown once
GET/api/projects/:id/vps/databaseanyRaw Postgres status (no password)
POST/api/projects/:id/vps/database/rotateanyRotate the raw Postgres password
POST/api/projects/:id/vps/redisanyProvision raw Redis: idempotent
GET/api/projects/:id/vps/redisanyRaw Redis status (no password)
POST/api/projects/:id/vps/redis/rotateanyRotate the raw Redis password (recreates the container)
GET/api/projects/:id/vps/allowlistanyList IP/CIDR entries allowed to reach raw Postgres/Redis
POST/api/projects/:id/vps/allowlistanyAdd an entry: re-syncs the VPS firewall
DELETE/api/projects/:id/vps/allowlist/:eidanyRemove an entry: re-syncs the firewall
GET/api/projects/:id/vps/envanyList VPS-scoped env var names
POST/api/projects/:id/vps/envanySet a VPS-scoped env var (written into the compute .env on deploy)
POST/api/projects/:id/vps/deployanyBuild + start the project's docker-compose stack
GET/api/projects/:id/vps/deploymentsanyVPS deploy history
GET/api/projects/:id/vps/deployments/:didanyVPS deployment status + logs (live while building)
GET/api/projects/:id/vps/statusanyRunning compute services + raw Postgres/Redis container status
GET/api/projects/:id/vps/logsanyTail logs for a compute service (or postgres/redis)
Raw Postgres/Redis are public-by-default the moment an IP is allowlisted (TCP + TLS + a strong per-project password, gated by an IP allowlist at the host firewall). This is more permissive than the Studio-API-only model the regular project database uses, because external tooling like drizzle-kit needs a real connection string.
POST/api/projects/:id/vps/databaseany

The connection string is returned once, in the response body. Store it immediately.

curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/vps/database \
  -H "Authorization: Bearer deploro_pat_..."
POST/api/projects/:id/vps/allowlistany

Body: { cidr, label? }, for example 203.0.113.4 or 203.0.113.0/24.

curl -X POST https://api.deploro.com/api/projects/PROJECT_ID/vps/allowlist \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"cidr":"203.0.113.4","label":"my laptop"}'

Auth-as-a-Service

Sign-in for your app's own end users, separate from platform auth above. Mounted at /auth/:slug/*, where :slug is your project's slug. Supports email OTP, email+password, and 15 OAuth providers.

MethodPathAuthDescription
GET/auth/:slug/sessionpublicValidate an end-user's session
POST/auth/:slug/logoutpublicClear the end-user session
GET/auth/:slug/verify-emailpublicConfirm a manually-added user's email → redirects to Site URL
POST/auth/:slug/email/request-otppublicEmail a 6-digit code (if the 'email' provider is enabled)
POST/auth/:slug/email/verify-otppublicVerify the code → session cookie, marks email verified
POST/auth/:slug/email-password/signuppublic{ email, name? } → confirmation email. Clicking it signs the user in and redirects to your reset-password page (?reset_token=…&verified=1) to choose a password; no password is taken at signup. Independent identity from OTP
POST/auth/:slug/email-password/loginpublic{ email, password } → session cookie
POST/auth/:slug/email-password/request-resetpublicEmail a password-reset link (always {ok:true}, so it can't reveal which emails exist)
GET/auth/:slug/reset-password/:tokenpublicValidates the link, redirects to your app's reset-password page
POST/auth/:slug/email-password/resetpublic{ token, password }: revokes every existing session for that identity
GET/auth/:slug/:providerpublicInitiate OAuth: google/github/facebook/twitter/discord/microsoft/linkedin/slack/gitlab/twitch/spotify/reddit/dropbox/notion/apple/upwork
GET/POST/auth/:slug/:provider/callbackpublicExchange code, upsert user, issue session
GET/api/projects/:id/auth/providers[/:provider]anyConfigure per-project provider credentials
PUT/api/projects/:id/auth/providers/:provideranySet a provider's client id/secret
GET/api/projects/:id/auth/usersanyList this project's end users
POST/api/projects/:id/auth/usersanyManually add an email-identity end user (sends a confirmation email)
DELETE/api/projects/:id/auth/users/:userIdanyDelete an end user
GET/api/projects/:id/auth/settingsanyGet the project's Auth Site URL
PUT/api/projects/:id/auth/settingsanySet the Site URL: where auth emails redirect end users back to
GET/api/projects/:id/auth/statsanyTotals for the dashboard
Signing users in from your own server. Sign-in, signup, code and reset requests are rate limited per IP address. If your backend makes these calls for your users, every user arrives from your server's address and shares one limit. Send two extra headers so each user is counted separately: X-Deploro-Server-Token, a personal access token with write scope for this project, and X-Deploro-Client-IP, the end user's IPv4 or IPv6 address. Without a valid token the IP header is ignored. An invalid token returns 401, a token for another project 403, and a malformed address 400. The per-account limit (10 failed attempts per 15 minutes) applies either way, and all server-forwarded requests for a project share a ceiling of 6,000 per 15 minutes. Keep the token on your server; never send it to a browser.
curl -X POST https://api.deploro.com/auth/my-app/email-password/login \
  -H "Content-Type: application/json" \
  -H "X-Deploro-Server-Token: deploro_pat_..." \
  -H "X-Deploro-Client-IP: 203.0.113.9" \
  -d '{"email":"user@example.com","password":"..."}'
POST/auth/:slug/email/request-otppublic

Silently no-ops for disposable email domains. Requires the email provider to be enabled for the project.

curl -X POST https://api.deploro.com/auth/my-app/email/request-otp \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}'
PUT/api/projects/:id/auth/providers/:providerany

Body: { client_id, client_secret, enabled, extra_config? }. Apple additionally needs extra_config.team_id and extra_config.key_id, with the .p8 private key contents as client_secret.

curl -X PUT https://api.deploro.com/api/projects/PROJECT_ID/auth/providers/google \
  -H "Authorization: Bearer deploro_pat_..." \
  -H "Content-Type: application/json" \
  -d '{"client_id":"...","client_secret":"...","enabled":true}'
Looking for the same operations from a terminal instead of curl? See the CLI Reference. Every command maps to one or more of the routes above.