Hosted ODS (one-click provider)
Goal: Host here provisions a real Spaces-capable ODS for an organization, so onboarding (including Elastic Projects dogfooding itself) does not require bringing your own Docker host.
What Vercel can and cannot do
| Approach | Fit for a PDS / ODS? |
|---|---|
| Next.js on Vercel (current app) | Yes for the Elastic Projects host UI and APIs |
Dockerfile.vercel / container Functions |
HTTP servers on Fluid compute. Scale to zero, function duration limits, no persistent disk. Fine for stateless APIs, not a durable AT Protocol PDS |
| Vercel Services (multi-service project) | Good for Next + a sidecar HTTP API in one deploy. Still Fluid semantics — not always-on machines with attached volumes |
| Vercel Sandbox | Ephemeral microVMs for untrusted code. Wrong lifetime for an org PDS |
Conclusion: Keep Elastic Projects (Next.js) on Vercel. Do not try to run
ghcr.io/bluesky-social/atproto:pds-spaces-alpha as the main app Dockerfile.
That image expects a long-lived process and a data volume.
Target onboarding shape
Sign in with Spaces
→ Create organization
→ Host here (one click)
→ Elastic Projects provisions an ODS
→ Org gets a pdsUrl + DID that can mint project spaces
→ Create project / push files (including EP → EP)
Provider model
HOSTED_ODS_PROVIDER selects the backend:
| Value | Behavior |
|---|---|
preview (default) |
Marks the org hosted in our SQLite index only. No real PDS process. Good for UI dogfood |
fly |
Creates a Fly Machine (or reuses one) running the Spaces alpha PDS image, attaches a volume, stores pdsUrl on the org |
Other always-on hosts (Render, Railway, a future Vercel always-on product) can
implement the same HostedOdsProvider interface.
Why not one shared Vercel container for every org?
A Bluesky PDS deployment is typically one hostname / one data directory. One-click per organization implies N isolated runtimes (or a custom multi-tenant Spaces fork). Per-org machines on Fly (or similar) match the Docker pack we already generate for self-host.
A later phase could add a shared Elastic Projects Spaces gateway as a Vercel Service for a soft multi-tenant subset. That is new protocol/product work, not a drop-in of the upstream PDS image.
Env for Fly provider
HOSTED_ODS_PROVIDER=fly
FLY_API_TOKEN=...
FLY_APP_NAME=elasticprojects-ods
FLY_REGION=iad
# Optional shared secrets for provisioned machines:
# FLY_ODS_JWT_SECRET=
# FLY_ODS_ADMIN_PASSWORD=
Create an empty Fly app once (fly apps create elasticprojects-ods). The
hosting API creates machines under that app when someone clicks Host here.
Transfer out (no lock-in)
Hosting an org's ODS must never mean owning it. AT Protocol already defines account migration, so transfer later is a supported path rather than a favor we grant:
com.atproto.server.createAccounton the destination host.com.atproto.sync.getRepohere →importRepothere (the CAR is the repo).- Copy blobs (
listBlobs/getBlob→uploadBlob) and preferences. - Sign a
did:plcoperation moving the DID's PDS endpoint to the new host. - Deactivate the account here once the destination serves reads.
The one thing that decides whether this is real
Step 4. A did:plc can only be repointed by a rotation key. If Elastic
Projects holds the only rotation key for an org's DID, that org cannot leave
without our cooperation, and every other guarantee is theater.
So the rule for hosted orgs is: the org holds a rotation key we do not
control. We may hold a second one for operational recovery, but ours is never
the only one. An org that wants to move does not need our signature, our
uptime, or our goodwill — it needs its own key. Same reason a did:web org
should keep the domain in its own registrar account.
Status in this codebase
| Piece | State |
|---|---|
| Per-org isolated runtime (Fly machine + volume) | Implemented in lib/ods/hosted-provider.ts |
| Self-host escape hatch (Docker pack) | Implemented (Local pack) |
| Point an org at an ODS someone else runs | Implemented (external hosting + pdsUrl) |
| Org-held rotation key at provision time | Not yet — required before hosting real orgs |
| One-click export / migrate to another host | Not yet — currently the manual CAR + blob + PLC flow above |
Spaces caveat: the migration flow above is well-trodden for personal repos. Repos held inside a space are alpha, and moving an org's spaces between hosts is not yet a paved path upstream. Until it is, treat transfer of a hosted org as: the DID and its repos move cleanly, and space membership may need to be re-established on the destination.
Local pack (unchanged)
Local pack still downloads docker-compose.yml for
ghcr.io/bluesky-social/atproto:pds-spaces-alpha so teams can run compute
themselves. That path stays the escape hatch when you do not want Elastic
Projects to operate the machine.