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.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/setup | public | One-time admin bootstrap: blocked once any user exists |
| POST | /api/auth/login | public | Sign in with email + password → sets session cookie |
| POST | /api/auth/logout | public | Clears the session cookie |
| GET | /api/auth/session | public | Current user from the JWT, or null |
| POST | /api/auth/request-otp | public | Email a 6-digit passwordless sign-in code (existing users only) |
| POST | /api/auth/verify-otp | public | Verify the code → session cookie, marks email verified |
| GET | /api/auth/oauth/status | public | Which platform OAuth providers (google/apple/linkedin) are configured |
| GET | /api/auth/oauth/:provider/start | public | Returns the provider's consent-screen URL |
| GET/POST | /api/auth/oauth/:provider/callback | public | Exchanges the code and signs in, but only if the email already has an account |
| GET | /api/auth/users | admin | List platform users |
| DELETE | /api/auth/users/:id | admin | Delete a user |
| PATCH | /api/auth/users/:id/role | admin | Set a user's role |
| PATCH | /api/auth/users/:id/ban | admin | Ban/unban: takes effect immediately, no session revocation needed |
| POST | /api/auth/invite | admin | Create an invite → returns acceptUrl |
| POST | /api/auth/accept-invite | public | Accept an invite token, create an account |
| GET | /api/auth/sessions | admin | List sessions (always [], since sessions are stateless JWTs) |
| DELETE | /api/auth/sessions/:id | admin | Revoke a session (no-op for the same reason) |
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":"..."}'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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects | any | List your projects |
| POST | /api/projects | any | Create a project (no database until provisioned) |
| GET | /api/projects/:id | any | Project details |
| PATCH | /api/projects/:id | admin | Update project settings |
| DELETE | /api/projects/:id | admin | Delete a project and its database |
| GET | /api/projects/:id/members | any | List members |
| POST | /api/projects/:id/invite | admin | Invite a member by email |
| DELETE | /api/projects/:id/members/:uid | admin | Remove a member |
Body fields (Zod-validated): name (1-50 chars), slug (2-30 chars, lowercase alphanumeric + hyphens), optional git_repo_url, git_branch.
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"}'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).
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/db/tables | any | List project tables |
| POST | /api/db/tables | admin | Create table (structured column defs → CREATE TABLE) |
| PATCH | /api/db/tables/:name | admin | Rename table |
| GET | /api/db/tables/:name | any | Paginated rows |
| DELETE | /api/db/tables/:name | admin | Drop table |
| GET | /api/db/schema/:table | any | Column info |
| POST | /api/db/query | admin | Raw SQL: read-only SELECT only, DDL blocked, audit logged |
| POST | /api/db/rows/:table | any | Insert row |
| PATCH | /api/db/rows/:table/:id | any | Update row |
| DELETE | /api/db/rows/:table/:id | any | Delete row |
/api/projects/:id.
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%"]}'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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/migrations | any | List migrations |
| POST | /api/projects/:id/migrations | any | Create a migration (name, sql_up, optional sql_down) |
| POST | /api/projects/:id/migrations/:mid/apply | any | Apply: runs sql_up in a transaction |
| POST | /api/projects/:id/migrations/:mid/rollback | any | Rollback: runs sql_down |
| DELETE | /api/projects/:id/migrations/:mid | any | Delete an unapplied migration |
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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/storage | any | List R2 objects |
| POST | /api/projects/:id/storage/upload | admin | Upload a file: 100MB max, multipart/form-data, optional bucket field |
| GET | /api/projects/:id/storage/download?key=... | any | Download a file (key as query param) |
| DELETE | /api/projects/:id/storage?key=... | admin | Delete a file (key as query param) |
| GET | /api/projects/:id/storage/buckets | any | List buckets |
| POST | /api/projects/:id/storage/buckets | admin | Create a bucket {name, public?} |
| PATCH | /api/projects/:id/storage/buckets/:name | admin | Toggle {public} |
| DELETE | /api/projects/:id/storage/buckets/:name | admin | Remove the policy (keeps files) |
| POST | /api/projects/:id/storage/signed-url | any | {key, expiresIn?}: time-limited public URL (max 7d, default 1h) |
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">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"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}'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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/deployments | any | Deployment history (Worker/Pages only) |
| POST | /api/projects/:id/deployments | any | Trigger a manual deploy |
| GET | /api/projects/:id/deployments/:did | any | Get a deployment + logs (resolves Worker/Pages or VPS) |
| GET | /api/projects/:id/deployments/all | any | Unified 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.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /webhooks/github | public | Inbound GitHub push handler: verified against the project's own webhook secret |
| GET | /api/projects/:id/github/webhook | any | This project's webhook URL + secret (generated lazily), plus auto_deploy_enabled |
| POST | /api/projects/:id/github/webhook/rotate | any | Issue a new secret, invalidating the old one immediately |
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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/domains | any | List custom domains |
| POST | /api/projects/:id/domains | any | Add a domain → returns CNAME + TXT instructions |
| POST | /api/projects/:id/domains/:did/verify | any | Verify TXT → registers CF custom hostname (auto DV SSL) |
| GET | /api/projects/:id/domains/:did/status | any | Poll SSL status (self-heals failed registration) |
| DELETE | /api/projects/:id/domains/:did | any | Remove a domain |
| POST | /api/projects/:id/provisional-domain | any | (Re)provision the {slug}.deploro.app subdomain, idempotent |
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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/realtime | any | WebSocket 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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/webhooks | any | List webhooks |
| POST | /api/projects/:id/webhooks | any | Create a webhook |
| PATCH | /api/projects/:id/webhooks/:wid | any | Update a webhook |
| DELETE | /api/projects/:id/webhooks/:wid | any | Delete a webhook |
| POST | /api/projects/:id/webhooks/:wid/test | any | Fire a test payload |
| GET | /api/projects/:id/webhooks/:wid/deliveries | any | Last 30 delivery records |
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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/studio/spec | any | OpenAPI 3.1 spec generated from the current schema |
| GET | /api/projects/:id/studio/:table | any | List rows (filterable via query params) |
| GET | /api/projects/:id/studio/:table/:id | any | Get one row by primary key |
| POST | /api/projects/:id/studio/:table | any | Insert a row |
| PATCH | /api/projects/:id/studio/:table/:id | any | Update a row |
| DELETE | /api/projects/:id/studio/:table/:id | any | Delete 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.
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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects/:id/observability/audit | any | Paginated SQL audit log: every query run against /api/db/query |
Hosting
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/hosting/status | any | Worker info + VPS health (admins also get untrackedContainers) |
| GET | /api/hosting/metrics | any | Last 60 VPS metric snapshots (cpu/mem/disk) |
| GET | /api/hosting/env | any | List Worker secret key names: values are never returned |
| POST | /api/hosting/env | any | Set 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.
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.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/projects/:id/vps/database | any | Provision raw Postgres: idempotent; connection string shown once |
| GET | /api/projects/:id/vps/database | any | Raw Postgres status (no password) |
| POST | /api/projects/:id/vps/database/rotate | any | Rotate the raw Postgres password |
| POST | /api/projects/:id/vps/redis | any | Provision raw Redis: idempotent |
| GET | /api/projects/:id/vps/redis | any | Raw Redis status (no password) |
| POST | /api/projects/:id/vps/redis/rotate | any | Rotate the raw Redis password (recreates the container) |
| GET | /api/projects/:id/vps/allowlist | any | List IP/CIDR entries allowed to reach raw Postgres/Redis |
| POST | /api/projects/:id/vps/allowlist | any | Add an entry: re-syncs the VPS firewall |
| DELETE | /api/projects/:id/vps/allowlist/:eid | any | Remove an entry: re-syncs the firewall |
| GET | /api/projects/:id/vps/env | any | List VPS-scoped env var names |
| POST | /api/projects/:id/vps/env | any | Set a VPS-scoped env var (written into the compute .env on deploy) |
| POST | /api/projects/:id/vps/deploy | any | Build + start the project's docker-compose stack |
| GET | /api/projects/:id/vps/deployments | any | VPS deploy history |
| GET | /api/projects/:id/vps/deployments/:did | any | VPS deployment status + logs (live while building) |
| GET | /api/projects/:id/vps/status | any | Running compute services + raw Postgres/Redis container status |
| GET | /api/projects/:id/vps/logs | any | Tail logs for a compute service (or postgres/redis) |
drizzle-kit needs a real connection string.
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_..."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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /auth/:slug/session | public | Validate an end-user's session |
| POST | /auth/:slug/logout | public | Clear the end-user session |
| GET | /auth/:slug/verify-email | public | Confirm a manually-added user's email → redirects to Site URL |
| POST | /auth/:slug/email/request-otp | public | Email a 6-digit code (if the 'email' provider is enabled) |
| POST | /auth/:slug/email/verify-otp | public | Verify the code → session cookie, marks email verified |
| POST | /auth/:slug/email-password/signup | public | { 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/login | public | { email, password } → session cookie |
| POST | /auth/:slug/email-password/request-reset | public | Email a password-reset link (always {ok:true}, so it can't reveal which emails exist) |
| GET | /auth/:slug/reset-password/:token | public | Validates the link, redirects to your app's reset-password page |
| POST | /auth/:slug/email-password/reset | public | { token, password }: revokes every existing session for that identity |
| GET | /auth/:slug/:provider | public | Initiate OAuth: google/github/facebook/twitter/discord/microsoft/linkedin/slack/gitlab/twitch/spotify/reddit/dropbox/notion/apple/upwork |
| GET/POST | /auth/:slug/:provider/callback | public | Exchange code, upsert user, issue session |
| GET | /api/projects/:id/auth/providers[/:provider] | any | Configure per-project provider credentials |
| PUT | /api/projects/:id/auth/providers/:provider | any | Set a provider's client id/secret |
| GET | /api/projects/:id/auth/users | any | List this project's end users |
| POST | /api/projects/:id/auth/users | any | Manually add an email-identity end user (sends a confirmation email) |
| DELETE | /api/projects/:id/auth/users/:userId | any | Delete an end user |
| GET | /api/projects/:id/auth/settings | any | Get the project's Auth Site URL |
| PUT | /api/projects/:id/auth/settings | any | Set the Site URL: where auth emails redirect end users back to |
| GET | /api/projects/:id/auth/stats | any | Totals for the dashboard |
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":"..."}'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"}'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}'