Build A Serverless App Backend
This guide shows the shape of a Storail backend for general app data.
The goal is to replace the parts of a traditional backend that store low-frequency app objects, user-owned records, object metadata, write permissions, and audit history. The app can be deployed quickly because the stack is contracts, subgraph, relay, SDK, and frontend. There is no application server to operate, and writes are paid for only when they happen.
The current SDK is still evolving. Treat this as the project structure and operation flow, not as a final API reference.
1. Choose App Records
Start with records that are useful as public or auditable app state.
Examples:
- user profile
- avatar
- user asset
- project record
- public app setting
- attachment metadata
Avoid using this path for high-frequency counters, private data, or data that needs hidden server-side logic.
2. Choose Path Conventions
Use paths that make ownership and query patterns explicit.
Examples:
/<user-address>/profile
/<user-address>/profile/avatar
/<user-address>/assets/<asset-id>
/<app-address>/objects/<object-id>
/<app-address>/projects/<project-id>
The first segment is the namespace owner. The owner can write under that namespace and can grant writers to a path prefix.
3. Store Object Bytes
Upload the object to Storacha or another content-addressed provider.
Storacha integration is not implemented in the SDK yet.
Placeholder shape:
const uploaded = await storage.put(file);
Expected result shape:
{
providerId: "storacha",
pointer: "bafy...",
contentHash: "0x...",
}
The app should verify content hashes on download. The storage provider is not a source of correctness.
4. Publish The Object Reference
Record the object reference in AuthorizedEventHub.
await client.publish({
path: `/${userAddress}/profile/avatar`,
providerId: uploaded.providerId,
pointer: uploaded.pointer,
contentHash: uploaded.contentHash,
metadata: JSON.stringify({
contentType: "image/png",
size,
}),
});
The relay path is the default. Use { mode: "direct" } only when the user should pay gas directly.
5. Update Or Remove Objects
Use the same path for replacement:
await client.update({
path,
providerId,
pointer,
contentHash,
metadata,
});
Remove the active reference:
await client.remove({ path });
The event history remains on-chain. The subgraph derives the current visible record.
6. Delegate Writes
Grant a writer for a specific path prefix:
await client.grantWriter({
domain: `/${userAddress}/assets`,
writer,
});
The writer can mutate that domain and child paths. The namespace owner keeps authority.
7. Read Through The Subgraph
Use the public-domain subgraph to list records and current pointers.
Typical queries:
- profile object by path
- assets under a user prefix
- writer permissions for a domain
- recent mutations for audit views
Wait for indexed before assuming the subgraph read model has caught up to a mutation.
8. Frontend Operation Handling
Use SDK operation states:
submitted -> confirmed -> indexed
confirmed means the write is on-chain. indexed means the subgraph can serve the updated read model.
For app backend UX:
- show the local upload immediately
- show
confirmedwhen the AEH mutation is mined - refetch lists after
indexed