Every request is authenticated and authorized independently. Internal services validate the same kind of opaque token the browser sends, with scopes reduced to what the calling user actually has. Network location and prior hops grant no trust.
Every authentication token is a high-entropy opaque random string, hashed at rest, and validated by database lookup. Tokens carry no embedded claims — claims come from the database row. Revoking a token is a single-row delete that takes effect on the next request.
Presented authentication tokens are hashed before database lookup, so plaintext tokens are not stored or compared directly. Where direct secret comparisons are required (CSRF token verification, auth-header checks, device-token equality), PageSpace uses a timing-safe helper so length, prefix structure, and content differences cannot leak through timing.
Internal services — web, realtime, processor — authenticate each other with opaque service tokens validated by the same session service that validates user sessions. When one service calls another on behalf of a user, it requests a service token scoped to a specific resource (e.g., "write files on page X") and the token is minted only if the user actually has that permission. The service token carries the user's identity forward; downstream routes assert the exact scope they need.
Service tokens are tracked in a central revocation registry that fails closed: presenting a token whose identifier is unknown, expired, or explicitly revoked is rejected. A maintenance job prunes expired entries without losing the revocation semantic.
Web sessions are a single opaque token, stored only as a hash on the server and as an HttpOnly, Secure, SameSite=strict cookie on the browser. Sessions are validated on every request — cookie, lookup, expiry check, user-suspension check, optional idle-timeout check — and are revoked by deleting the row.
Device tokens — desktop and mobile clients — rotate on refresh. When the client presents its current token, the server issues a new one, keeps the old one valid for a brief grace window so concurrent requests don't fail, and marks the old one replaced. Rapid-refresh anomalies combined with user-agent or IP changes lower the device's trust score; a low enough score lets an operator force re-authentication.
Administrative actions — log-out-everywhere, credential reset, account suspension — invalidate every active session for a user atomically. In-progress tokens are rejected on their next request.
Login, signup, magic-link send, and token-refresh endpoints are rate-limited per-IP, per-email, and per-user depending on the endpoint, using a distributed sliding-window counter stored in Postgres. Exceeding a bucket returns HTTP 429 with Retry-After. Progressive blocking extends the ban for repeated violators. In production, a storage-layer failure fails closed rather than opening the floodgates.
Authentication, authorization, data-access, and admin events are recorded in a SHA-256 hash-chained log — every entry's hash incorporates the previous entry's hash, so any retroactive change to history is detectable. A database-level lock serializes chain writes so concurrent inserts cannot fork the chain.
The chain is continuously re-verified:
Each audit entry carries the user reference (nullable for unauthenticated failures), IP, user agent, session reference, a resource reference where applicable, and a risk score for downstream alerting. Email addresses are redacted before write — enough structure is preserved for operational debugging without storing full PII.
State-changing requests must present a CSRF token bound to the current session.
GET /api/auth/csrf.x-csrf-token header.Every server-side URL fetch runs through a validator that resolves the hostname, then checks every resolved IP against a blocklist before connecting.
The blocklist covers:
A single hostname can resolve to multiple IPs; the validator rejects the request if any resolved address is blocked — defeating DNS-rebinding tricks where a host advertises a public IP but serves a private one. IPv4-mapped IPv6 is normalized before comparison.
Filesystem paths from uploads, avatars, and user-controlled input run through a validator before the filesystem is touched.
Blocked:
Attachment uploads treat the processor response as an untrusted cross-service claim. The web layer independently hashes the received bytes and rejects processor responses whose SHA-256 content hash or byte size does not match before it creates file metadata or authorization linkages.
The file store is content-addressable, so identical bytes can deduplicate across contexts. Deduplication does not grant access by itself: files are served only through authorized page or conversation linkages, and a new upload creates only the linkage the caller is allowed to create.
All attachment targets pass the upload-time content detector that rejects executables, HTML, SVG, and scripts based on detected bytes rather than extensions. Channel uploads also enqueue page-backed ingest jobs for OCR and extraction. DM uploads currently stop after storage and linkage because they have no page row for the ingest worker; policy decisions that depend on post-storage DLP or text extraction should treat DM attachments as a narrower processing surface until DM-safe ingestion exists.
User-generated HTML is sanitized before render. Canvas pages render inside Shadow DOM, isolating arbitrary author styles and scripts from the rest of the app. Mentions and links are validated server-side before rendering.
OAuth tokens for connected integrations and other application-layer secrets are encrypted at rest with AES-256-GCM using scrypt-derived keys, a unique salt per write, and a unique IV per encryption. Stored secrets are decrypted only on the request path that needs them and are never returned in API responses. AI provider credentials are held by the deployment operator at the infrastructure layer and are not stored per user.
Search docs, blog posts, and more.