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:

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:

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: