AppTheory Core Patterns
This document records the canonical patterns AppTheory expects across languages and documentation surfaces.
Pattern: treat API snapshots as the external source of truth
Problem: human docs can drift from exported interfaces.
CORRECT:
- update docs and
api-snapshots/*in the same change when an external API moves - treat
api-snapshots/go.txt,api-snapshots/ts.txt, andapi-snapshots/py.txtas release-gated truth - mark unconfirmed interfaces as
UNKNOWN:orTODO:instead of guessing
INCORRECT:
- documenting an export that is not present in the snapshots
- inferring public stability from an internal helper or a test-only path
Pattern: keep Lambda entrypoints thin
Problem: hand-rolled event-shape detection drifts from the runtime contract.
CORRECT:
func handler(ctx context.Context, event json.RawMessage) (any, error) {
return app.HandleLambda(ctx, event)
}
INCORRECT:
if event.RequestContext.HTTP.Method != "" {
return app.ServeAPIGatewayV2(ctx, parsed)
}
The dispatcher already knows how to route HTTP, queues, streams, WebSockets, and other supported AWS shapes. That includes standard AppSync resolver events.
Pattern: route AppSync resolvers through the normal router
Problem: bespoke GraphQL field switching duplicates request adaptation and bypasses the runtime’s typed AppSync context.
CORRECT:
app.Get("/getThing", func(ctx *apptheory.Context) (*apptheory.Response, error) {
appsync := ctx.AsAppSync()
return apptheory.JSON(200, map[string]any{"field": appsync.FieldName})
})
Use ServeAppSync / serveAppSync / serve_appsync for AppSync-only Lambdas, or keep mixed-trigger Lambdas on the
universal dispatcher.
INCORRECT:
switch event.Info.FieldName {
case "getThing":
// bespoke resolver handling outside the AppTheory router
}
Pattern: header handling is case-insensitive and response keys are lowercase
Problem: HTTP header names are case-insensitive, but maps and dicts are not.
CORRECT:
reqID := ctx.Header("X-Request-Id")
resp.Headers["x-request-id"] = []string{reqID}
INCORRECT:
resp.Headers["X-Request-Id"] = []string{reqID}
Pattern: register more-specific routes first
If two routes are equally specific, the router prefers earlier registration order.
CORRECT:
app.Get("/users/me", handleMe)
app.Get("/users/{id}", handleUser)
INCORRECT:
app.Get("/users/{id}", handleUser)
app.Get("/users/me", handleMe)
Pattern: use strict route registration in tests and CI
Default route registration preserves backwards compatibility. Invalid patterns can therefore be ignored unless you opt into strict registration.
CORRECT:
if _, err := app.GetStrict("/users/{id}", handleUser); err != nil {
panic(err)
}
INCORRECT:
app.Get("/{proxy+}/x", handleUser)
Pattern: keep middleware pure and deterministic
CORRECT:
- store request-scoped values via
ctx.Set(...)/ctx.Get(...) - return a modified response rather than mutating global state
INCORRECT:
- caching per-request values in package globals
- depending on wall-clock time instead of the injected clock or test env
Pattern: streaming is adapter-specific
Streaming is validated by contract fixtures for supported adapters. Do not assume every AWS integration supports the same streaming semantics.
CORRECT:
- use runtime-provided helpers such as
htmlStream,sseEventStream,SSEResponse, orSSEStreamResponse - test streaming deterministically with the language test env
INCORRECT:
- assuming every AWS integration supports streaming the same way
Pattern: choose the MCP deployment shape by client transport needs
Problem: Bedrock AgentCore and Claude Remote MCP do not share the same transport, streaming, or OAuth discovery requirements.
CORRECT:
- use
AppTheoryMcpServerfor AgentCore or other POST-only MCP clients on HTTP API v2 - use
AppTheoryRemoteMcpServerplusAppTheoryMcpProtectedResourcefor Claude Remote MCP when you needPOST/GET/DELETE /mcp, OAuth protected-resource discovery, and a REST API v1 streaming edge - wire
mcp.NewDynamoStreamStore(db)or another persistentStreamStorein application code if replay must survive reconnects and cold starts; forDynamoStreamStore, use a standard TableTheory DB withTransactWritein production so delete/append races are guarded atomically, and letMCP_STREAM_TTL_MINUTESdefine the runtime replay window - wire
mcp.NewDynamoSessionStore(db)when sessions must survive cold starts; session writes are upserts so TTL refresh does not depend on a delete/recreate cycle - wire
mcp.NewDynamoTaskStore(db)throughmcp.WithTaskRuntime(...)only when asynchronous tool work needs durable task state and product policy is ready; task state is session-scoped and must remain bound to the same principal, tenant, actor route, and entitlement policy as the MCP session - enforce route-, principal-, and tool-aware MCP rate limits through
runtime.RateLimitMiddleware(...)andpkg/limitedin the normal AppTheory middleware chain; this is product wiring around the MCP handler, not a separate MCP framework feature - let the Remote MCP construct provide the stream table plus S3 spill bucket for durable large logical events; clients
still replay by logical
Last-Event-ID, and AppTheory bounds S3 spill reads before byte-count/hash validation - treat tool panics as server faults: AppTheory recovers them into sanitized JSON-RPC internal errors, not client-visible panic strings
- keep optional MCP utility capabilities fail-closed: completions are advertised only by AppTheory after a matching hook is configured, resource subscription and logging methods are hook-gated but their capabilities remain omitted until outbound notification contracts exist, task capability is advertised only after a task store and task-capable tool are configured, and cancellation notifications only cancel tracked in-flight requests for the same session
INCORRECT:
- assuming
AppTheoryMcpServeris a drop-in deployment for resumable Remote MCP - assuming
enableStreamTablealone makes replay durable withoutmcp.WithStreamStore(...) - assuming
enableTaskTablealone advertises MCP tasks withoutmcp.WithTaskRuntime(...)and a task-capable tool - splitting tool results or returning object links to work around stream-store storage limits
- depending on panic text or duplicate session-create failures as part of product behavior
- hard-coding
resources.subscribe,logging,completions, ortasksin a product wrapper before product authorization, tenant policy, quotas, audit logging, and abuse controls are wired - creating an MCP-specific rate-limit wrapper or construct flag instead of using
RateLimitMiddlewarewith scopedpkg/limitedbuckets
Pattern: sanitize user payloads before logging
Import pipelines and event-driven workloads often process PCI/PII-heavy payloads. Treat payloads as user data and prevent sensitive data leaks in logs.
CORRECT:
- sanitize log strings to strip control characters
- sanitize structured fields by key name
- log sanitized JSON or XML instead of raw payload dumps
INCORRECT:
- logging raw request bodies or third-party payloads directly
- assuming internal batch jobs can skip sanitization
Guide: Sanitization
Pattern: use the jobs ledger for long-running import workflows
CORRECT:
- create a job record before fan-out starts
- use record-level status plus idempotency keys for retries
- acquire and refresh leases when work can be retried concurrently
INCORRECT:
- relying on at-least-once delivery without idempotency state
- storing job progress only in logs
Guide: Jobs Ledger