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 handlerWhat each layer decides — and what it can reject the request for
| Layer | Decision | Rejection status |
|---|---|---|
| CORS | Is this origin allowed for credentialed requests? Outermost so even rejected requests get the right preflight response. | 403 on disallowed origin. |
| RequestID | Stamps a UUID on the request and echoes it on the response so clients and server logs can correlate. | Never rejects. |
| CSRF | Cookie-authenticated requests must carry a matching CSRF token. API-key requests skip this layer. | 403 on missing or mismatched token. |
| TenantResolver | Reads X-Tenant-ID (if present) and the credential's tenant claim, then attaches the resolved tenant to the request state. | 403 on tenant mismatch. |
| ProjectResolver | For 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. |
| Audit | Writes 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. |
| SecurityHeaders | Sets response-side headers: CSP, X-Frame-Options, HSTS, X-Content-Type-Options. | Never rejects. |
The two credential paths
| Header | Used by | How 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
| Header | Required when | Set by |
|---|---|---|
Authorization or X-API-Key | Always, except /health, /auth/login, /auth/register. | Caller. |
X-Project-ID | Every /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-ID | When a service-auth caller is acting on behalf of an end user. Composed as "{tenant_id}:{end_user_id}" internally. | Caller. |
X-Request-ID | Optional 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_idandproject_id. If they are not what you expected, the resolver chose differently — usually because the credential's claim won over the header.