Overview

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, and api-snapshots/py.txt as release-gated truth
  • mark unconfirmed interfaces as UNKNOWN: or TODO: 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, or SSEStreamResponse
  • 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 AppTheoryMcpServer for AgentCore or other POST-only MCP clients on HTTP API v2
  • use AppTheoryRemoteMcpServer plus AppTheoryMcpProtectedResource for Claude Remote MCP when you need POST/GET/DELETE /mcp, OAuth protected-resource discovery, and a REST API v1 streaming edge
  • wire mcp.NewDynamoStreamStore(db) or another persistent StreamStore in application code if replay must survive reconnects and cold starts; for DynamoStreamStore, use a standard TableTheory DB with TransactWrite in production so delete/append races are guarded atomically, and let MCP_STREAM_TTL_MINUTES define 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) through mcp.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(...) and pkg/limited in 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 AppTheoryMcpServer is a drop-in deployment for resumable Remote MCP
  • assuming enableStreamTable alone makes replay durable without mcp.WithStreamStore(...)
  • assuming enableTaskTable alone advertises MCP tasks without mcp.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, or tasks in 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 RateLimitMiddleware with scoped pkg/limited buckets

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