Hosted data server
Every project on Elastic Projects is backed by a real AT Protocol repository. Elastic Projects hosts it here. It serves over standard atproto sync endpoints. This page explains what exists, how the storage works, and where the limits are.
What each project gets
| Piece | Value |
|---|---|
| Repo identity | did:web:<slug>.elasticprojects.com |
| Handle | <slug>.elasticprojects.com |
| DID document | https://<slug>.elasticprojects.com/.well-known/did.json |
| Handle proof | https://<slug>.elasticprojects.com/.well-known/atproto-did |
| Sync endpoint | https://elasticprojects.com/xrpc/com.atproto.sync.getRepo |
The slug comes from the project name plus a short hash of the space URI. It stays stable for the life of the project. The repo is created the first time someone calls a sync endpoint for it.
Fetching a repo
Spaces are permissioned. The sync endpoints take the project share token as the credential. It is the same capability that opens the /join page and the git clone URL. Grab the token from the share dialog, then:
# The signed repository as a CAR file
curl -o repo.car "https://elasticprojects.com/xrpc/com.atproto.sync.getRepo?did=did:web:SLUG.elasticprojects.com&token=TOKEN"
# The current head commit and revision
curl "https://elasticprojects.com/xrpc/com.atproto.sync.getLatestCommit?did=did:web:SLUG.elasticprojects.com&token=TOKEN"
The CAR file is a spec-correct atproto repository. Every commit is signed with
the repo secp256k1 key, the one published in its DID document. Any atproto
library can verify it. Each published file is a record in the
com.elasticprojects.file collection. The key is the path with
slashes turned into colons.
Two identity endpoints are public because they only say who a repo is, never what it holds:
curl "https://elasticprojects.com/xrpc/com.atproto.server.describeServer"
curl "https://elasticprojects.com/xrpc/com.atproto.identity.resolveHandle?handle=SLUG.elasticprojects.com"
Writing to a repo
Projects are closed communities. Writes use the same share-token capability as reads. The standard atproto record endpoints are live. A record written over XRPC matches a file published in the app. Same validation, same tree, same signed history.
# Create or replace a file. The rkey is the path with "/" as ":".
curl -X POST "https://elasticprojects.com/xrpc/com.atproto.repo.putRecord" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repo": "did:web:SLUG.elasticprojects.com",
"collection": "com.elasticprojects.file",
"rkey": "notes:hello.md",
"record": { "text": "# Hello from XRPC\n" }
}'
# Read one record back
curl "https://elasticprojects.com/xrpc/com.atproto.repo.getRecord?repo=did:web:SLUG.elasticprojects.com&collection=com.elasticprojects.file&rkey=notes:hello.md&token=TOKEN"
# Delete a file
curl -X POST "https://elasticprojects.com/xrpc/com.atproto.repo.deleteRecord" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repo": "did:web:SLUG.elasticprojects.com",
"collection": "com.elasticprojects.file",
"rkey": "notes:hello.md"
}'
com.atproto.repo.applyWrites batches up to 50 creates, updates, and deletes
into one signed commit:
curl -X POST "https://elasticprojects.com/xrpc/com.atproto.repo.applyWrites" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repo": "did:web:SLUG.elasticprojects.com",
"writes": [
{ "$type": "com.atproto.repo.applyWrites#create",
"collection": "com.elasticprojects.file",
"rkey": "plan:launch.md",
"value": { "text": "- [ ] pick a date\n" } },
{ "$type": "com.atproto.repo.applyWrites#delete",
"collection": "com.elasticprojects.file",
"rkey": "plan:old.md" }
]
}'
Writes enforce the same rules as the app editor. Text files only. Cap is 100 KB
per document. JSON must parse. Paths are normalized. Traversal is rejected.
Paths that cannot be record keys (for example Next.js bracket segments) get
hashed rkeys. Pass the real path in the record path field when writing those.
Writes are attributed to the project authority. The share token is a
project-level capability, not a personal one.
The ep CLI speaks these endpoints in remote mode. A whole directory can be
published or pulled as one batch. See the source control page
for the commands.
How the storage works
The repo store runs on object storage with a write-ahead log. That shape matches modern git hosts for repositories at scale. There is no relational database in the write path.
- Every commit is one immutable CAR slice in object storage. Slices are never rewritten.
- A small per-repo index object records the head commit and the list of log entries. Every publish updates it with an atomic compare-and-swap on its ETag. Two concurrent writers cannot both win. The loser reloads and retries. Publishes are linearizable without locks or consensus.
- Readers keep a warm in-memory copy and revalidate it with a conditional read. An unchanged answer costs one metadata round trip.
- When the log grows past a threshold, it is folded into a snapshot CAR. The next reader materializes from one snapshot instead of replaying the whole log. Compaction is opportunistic. Losing the swap means someone else published first.
In production the store is Vercel Blob. In development and tests it is a directory on disk with the same compare-and-swap semantics.
Honest limits
- The repo mirrors the published tree. The app database is still the store of record for drafts and members. The repo converges to it on every sync request. When native hosted accounts land, that flips. The log becomes primary.
- There is no firehose, by design. Projects are closed communities that do not sync amongst each other. Relays have nothing to subscribe to. Nothing here appears in the public network. That is also why every repo endpoint takes the share token. Private project files stay private.
- Binary files cannot be written over XRPC yet, only text records. Their bytes can be read:
com.atproto.sync.getBlob?did=…&cid=<sha256>&token=…returns them, where thecidis thesha256on the file record. - Account migration is not supported. The DIDs are
did:webunder our domain. They name repos hosted here, not portable accounts.