Implement the core.
Choose your own stack.
A client can be written in any programming language. What matters is the boundary between canonical protocol data, capability contracts, providers, local persistence, signing and durable message delivery.
Contract-first Backend architecture1. Canonical domain model
Define provider-independent entities before connecting any external APIs. A FID identifies a Farcaster user; a Cast represents a signed post; a Profile contains normalized user attributes; a Link encodes following relationships; and a MessageId identifies signed protocol messages. No entity should store provider-specific presentation fields as authoritative protocol state.
{
"profile": {
"fid": 1234,
"username": "example",
"displayName": "Example user",
"source": {
"providerId": "reader-1",
"contract": "snapchain-v1"
}
},
"capabilities": {
"profile.byFid": true,
"feed.global": false,
"posts.write": false
}
}This is an illustrative internal representation, not an actual Snapchain response or a published Genycaster JSON API.
2. Ports and capability contracts
Define interfaces by operation, not by vendor hostname. The application calls a normalized read port; each adapter maps the operation to its own wire protocol. A writer port accepts canonical signed messages and is never silently substituted by a read API.
| Port | Contract | Typical implementation |
|---|---|---|
| ReadGateway | Profile, cast, FID-scoped feed, follow links and paged results | Raw Snapchain adapter |
| IndexedGateway | Search, derived timelines, aggregate counts if supplied | Optional indexed API adapter |
| WriteGateway | Submit pre-signed message, observe confirmation | Validator / write-capable RPC |
| LocalStore | Read snapshots, upsert normalized entities, persist cursors | Transactional persistence |
| Signer | Sign canonical messages with authorized client key | Device-owned signing boundary |
A provider advertises capabilities explicitly. Do not guess compatibility from a successful network ping or a familiar brand name.
3. Capability-based routing algorithm
If no allowed provider satisfies the request, return an explicit unsupported_capability. Do not leak an FID or other user data to an unapproved fallback. Health, authorization and support are separate dimensions.
4. Cache, synchronization and cursors
Read the most recent local snapshot first, then schedule bounded remote refreshes. Persist canonical records and freshness metadata. A cursor is scoped to the provider ID, endpoint identity, query, filters and contract version; opaque cursors from different services must never be combined.
{
"route": {
"providerId": "reader-1",
"contract": "snapchain-v1",
"endpointFingerprint": "opaque-bound-identifier"
},
"query": { "capability": "casts.byFid", "fid": 1234 },
"cursor": "opaque-value-from-that-provider",
"fetchedAt": "ISO-8601 timestamp"
}Storage technology is an implementation choice: an embedded database, IndexedDB, server-side relational storage or another transactional engine can all work if they preserve stable IDs, provenance and atomic outbox updates.
5. Signed writes and durable delivery
Keep the signer boundary separate from readers. Obtain an authorized app key through a trusted custody flow, sign a canonical protocol message locally, persist a delivery record and pin the chosen validator endpoint identity before attempting to send.
Track pending, acknowledged, uncertain, rejected and confirmed states distinctly. An HTTP timeout is not proof that a signed message was not accepted.
6. Contracts to test in every implementation
- Raw providers cannot claim derived capabilities they do not expose.
- Strict mode produces zero extra provider calls.
- Fallback enrichment preserves the primary user's FID and canonical identity.
- Changing readers invalidates route-bound cursors and keeps prior data provenance.
- Local cache returns a consistent snapshot while disconnected.
- A queued signed message never changes its selected writer implicitly.
- Unknown confirmation state does not cause unsafe duplicate publishing.
- Credentials and signer secrets are not logged or sent through read-provider routes.
Reference architecture: system diagram · provider contracts · self-hosting and node health.