MemorySyncMemorySync
How It Works

Request Lifecycle

Every HTTP request to MemorySync passes through the same ordered middleware stack before it reaches a route handler — and through the tail end of the same stack on the way out. This page walks the stack from outside in, names every layer that touches the request, and tells you what each one decides.

The middleware stack, outermost to innermost

TEXT
inbound request
  └─ CORS                      handles preflight, sets Access-Control-* headers
     └─ Pacing                 non-blocking traffic pacing; never rejects
        └─ Instrumentation     captures timings, status, request size
           └─ ErrorHandler     converts uncaught exceptions to JSON errors
              └─ Logging       writes a structured request log line
                 └─ RequestID  attaches X-Request-ID to req + resp
                    └─ CSRF            for cookie-auth requests only
                       └─ ApiRequestLogging   per-call audit prep
                          └─ UsageTracking     counts bytes and rows for billing
                             └─ SessionActivity  bumps last-seen on the session
                                └─ TenantResolver   resolves tenant_id
                                   └─ ProjectResolver  validates X-Project-ID
                                      └─ Audit           writes audit-log row
                                         └─ SecurityHeaders  CSP, X-Frame, HSTS
                                            └─ route handler

What each layer decides — and what it can reject the request for

LayerDecisionRejection status
CORSIs this origin allowed for credentialed requests? Outermost so even rejected requests get the right preflight response.403 on disallowed origin.
RequestIDStamps a UUID on the request and echoes it on the response so clients and server logs can correlate.Never rejects.
CSRFCookie-authenticated requests must carry a matching CSRF token. API-key requests skip this layer.403 on missing or mismatched token.
TenantResolverReads X-Tenant-ID (if present) and the credential's tenant claim, then attaches the resolved tenant to the request state.403 on tenant mismatch.
ProjectResolverFor routes opted in via mark_router_no_project()'s opposite set, validates X-Project-ID belongs to the resolved tenant.400 missing header / 403 wrong tenant.
AuditWrites one audit row per mutating request with actor, action, resource_type, status, ip_address, user_agent.Never rejects — a logging failure cannot fail the parent request.
SecurityHeadersSets response-side headers: CSP, X-Frame-Options, HSTS, X-Content-Type-Options.Never rejects.

The two credential paths

HeaderUsed byHow it is validated
Authorization: Bearer <jwt>Dashboard and direct user clients.Decoded, signature checked, expiry checked, session looked up by family_id.
X-API-Key: <key>Server-to-server SDK calls.Hashed key looked up; tenant_id, environment, optional project_id attached to the request.

What routes require which headers

HeaderRequired whenSet by
Authorization or X-API-KeyAlways, except /health, /auth/login, /auth/register.Caller.
X-Project-IDEvery /memory/* route and every project-scoped read or write. Skipped on org-level admin endpoints.Caller (or implied by a project-locked API key).
X-End-User-IDWhen a service-auth caller is acting on behalf of an end user. Composed as "{tenant_id}:{end_user_id}" internally.Caller.
X-Request-IDOptional inbound; always present on the response.Caller (optional) or RequestID middleware.

The shape of the response, success and error

JSON
// success — varies per route
{
  "id": 18421,
  "content": "...",
  "embedding_version": "v3",
  "tier": "hot",
  "retention_status": "active",
  "created_at": "2026-05-04T10:14:32Z"
}

// error — uniform across the API
{
  "error": {
    "code": "PROJECT_REQUIRED",
    "message": "X-Project-ID header is required for this route",
    "request_id": "req_3f9c1ab2"
  }
}

How to read a request log line

Every request emits one structured log line at completion. Useful fields: request_id, tenant_id, user_id, route, method, status, duration_ms, bytes_in, bytes_out. Filter by request_id to trace one request across the API server, the worker pool, and the audit forwarder — all three carry the same id.

What to check when a request behaves unexpectedly

  • Did it reach the handler? If the rejection came from a middleware, the audit row will show action=request, status=<4xx>, but no business-event row. If it reached the handler, you will see a domain audit row (e.g. memory.add).
  • Which scope was applied? The log line carries the resolved tenant_id and project_id. If they are not what you expected, the resolver chose differently — usually because the credential's claim won over the header.