What the server SDK supplies
@capp/server-sdk supplies CappServer, route metadata, receipt/idempotency interfaces and gateway trust/replay verification. The Python SDK offers the corresponding core and a FastAPI adapter. These libraries run inside your backend; installing them does not provision one.
Use the workspace tarball instructions. The TypeScript server package currently depends on both @capp/contracts and @capp/app-sdk. Keep server imports and private configuration out of browser bundles.
What your application still owns
Your service owns user authentication, authorization, data validation, storage, billing and the business effect. A cApp installation is not a user; a user session is not a purchase entitlement; a signed request is not proof that a handler's business rule is correct.
A useful integration separates these layers:
Request path, method and size admission
↓
Existing user/session authentication
↓
Trusted install assertion and replay check, when required
↓
User-to-install/resource permission check
↓
Validated operation and durable application transaction
↓
Correlated response / acknowledgement / receiptThis is an application design boundary, not a pipeline automatically created by the constructor. A public frontend, including a native-wrapped one, must not contain backend-only provider keys or a gateway HMAC signing secret.
Publish demand and supply deliberately
| Contract | Owner and role |
|---|---|
| Application definition / backend demand | The application declares the routes and capabilities it needs. |
capp.server-contract.v1 | The backend describes routes it provides, their methods and policy metadata. |
capp.data-contract.v2 | The application/backend agree on collections, revisions and sync endpoints. |
| Compiled binding and native route policy | The reviewed build selects the actual service/origin and admitted routes. |
server.serverContract() creates the supply document. Serving it at /.well-known/capp/server-contract is an explicit framework route you add; the constructor does not create that endpoint. Expose only public policy metadata, not secrets, user receipts or trust-store contents. Python's binding_seed() produces a proposal, not an approved deployment binding.
Keep method and route pattern semantics consistent. /api/notes/:id is a cApp route pattern; a framework folder called [id] must be translated to it. A policy description such as auth: 'session' does not implement a session check.
A read-only framework adapter
For a Next-style Request/Response handler, this complete module can be exported as the application's GET route. It is intentionally a public, passive, synthetic read. It performs no mutation and is not an authenticated service example.
import { CappServer } from '@capp/server-sdk';
import { cappRoute } from '@capp/server-sdk/next';
export const catalogueServer = new CappServer({
serverId: 'example-catalogue', origin: 'https://api.example.com',
enforceGatewayAssertions: false,
});
export const GET = cappRoute(
catalogueServer, '/api/items', ['GET'],
{ capability: 'items.read', group: 'catalogue', risk: 'low', receiptRequired: false },
async () => Response.json({ items: [{ id: 'sample', title: 'Public sample' }] }),
);The adapter wraps a handler; it does not register your HTTP server or replace framework path/method dispatch. enforceGatewayAssertions: false is the constructor default and permits unverified/missing gateway identity. Use it only for intentionally passive/public integration, not to get a protected route working.
Verification must be configured explicitly
For protected routes, configure expected app/site/audience/binding as appropriate, an authenticated install trust registry and an atomic replay store. Enrol public install keys only through a trusted authentication/registration workflow. Never trust a public key merely because the same request asks you to trust it.
The following complete factory illustrates a literal-path protected read. It requires your own session, install-authorization, storage and data adapters. No public enrolment endpoint or permissive default is supplied.
import {
CappServer, CappPolicyError,
type CappGatewayAssertion, type GatewayTrustStore,
type GatewayReplayStore, type ReceiptStore,
} from '@capp/server-sdk';
type ProtectedItemsOptions = {
trustStore: GatewayTrustStore;
replayStore: GatewayReplayStore;
receiptStore: ReceiptStore;
authenticate: (request: Request) => Promise<string | null>;
authoriseInstall: (userId: string, gateway: CappGatewayAssertion) => Promise<boolean>;
readItems: (userId: string) => Promise<unknown>;
};
export function createProtectedItems(options: ProtectedItemsOptions) {
const server = new CappServer({
serverId: 'example-private-catalogue', origin: 'https://api.example.com',
enforceGatewayAssertions: true,
gatewayAudience: 'com.example.catalogue', gatewayAppId: 'com.example.catalogue',
gatewayTrustStore: options.trustStore, gatewayReplayStore: options.replayStore,
receiptStore: options.receiptStore,
});
const route = server.registerRoute('/api/items', ['GET'], {
capability: 'items.read', group: 'catalogue', risk: 'low',
defaultEnabled: true, receiptRequired: true,
});
async function handle(request: Request): Promise<Response> {
if (new URL(request.url).pathname !== '/api/items') return new Response(null, { status: 404 });
if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } });
try {
const userId = await options.authenticate(request);
if (!userId) return new Response(null, { status: 401 });
const gateway = await server.verifyRequest(route, request.method, request.headers);
if (!gateway.verified || !await options.authoriseInstall(userId, gateway)) {
return new Response(null, { status: 403 });
}
const body = JSON.stringify({ items: await options.readItems(userId) });
await server.emitReceipt({
route, method: 'GET', result: 'ok', gateway, subject: userId, responseBody: body,
});
return new Response(body, { headers: { 'Content-Type': 'application/json' } });
} catch (error) {
if (error instanceof CappPolicyError) {
return Response.json({ ok: false, error: 'Request not authorised' }, { status: error.statusCode });
}
throw error;
}
}
return { server, handle };
}Replace the example identity and origin with the actual deployment contract. The adapters are required application implementations, not functions supplied by cApp. A request with no trusted assertion must fail before readItems; a signature from an untrusted/revoked install must also fail. Bind the authenticated user to that installation rather than assuming any enrolled install may access any account.
For mutations, bound the request body before buffering it, retain the exact received bytes, and verify those bytes before parsing and acting. Re-serializing parsed JSON can produce a different body hash. For parameterized routes, additionally bind the concrete requested resource in your adapter: verifyRequest() is given the registered route pattern, not the actual request URL.
Verify once per request. Ed25519 verification consumes a replay identity; re-verifying the same assertion later in the same handler can correctly reject it as replayed. Pass the verified context forward instead.
Receipts, replay and durable effects
Gateway replay and business idempotency solve different problems. A native assertion's short-lived jti protects one transport attempt. Your stable order/mutation identity survives a retry with a fresh assertion. Do not use a new random order ID every time a network response is lost.
The included memory stores lose state at restart and do not coordinate multiple processes. idempotent(key, execute) performs get, execute, then put; it is not an atomic concurrent claim and does not compare changed arguments under the same key. Use application storage that atomically binds user, operation identity, request digest and outcome. An interrupted external effect requires reconciliation, not a blind rerun.
A receipt records the SDK's supplied result and hashes; it is not independent attestation from an external service. A storage failure after an effect can still leave an unknown outcome. Keep receipts private by default and redact metadata before sharing.
For local-first collection sync, return the correlated acknowledgement only after the durable transaction. Use HTTP 409 for conflicts. The builder does not create your sync database or conflict policy.
Payment-backed service example
A merchant first saves trusted request terms, acquires trusted chain observations and independently verifies recipient credit. It then atomically claims that payment for the intended order before its own delivery workflow acts. A client receipt, JSON shape or hash alone is insufficient.
The payment verification guide and portable verifier describe this reusable check. They do not provide the durable claim or order fulfilment transaction.
Deployment checks and failure handling
Test missing/expired/wrong-app assertions, altered body bytes, revoked installs and replay before enabling valuable routes. Test one request against every instance of a scaled service to establish shared trust/replay storage. Keep public web-session access separate from routes that require native install assertions; disabling all gateway enforcement is not a substitute for designing those access paths.
Expose diagnostic categories, not raw bearer tokens, signed assertions or private receipt metadata. A 401 can be missing authentication/trust; 403 can be route/resource policy; 409 can be replay or an application conflict. Interpret the response in its actual protocol rather than treating all 409s as a safe retry.
See server API, Python integration and credentials and authority.