Three-tier separation
Kindred is structured as three independent tiers: a Next.js frontend, a Python backend (FastAPI), and a managed Postgres database (Supabase). Each tier holds only the credentials and responsibilities it needs.
The frontend is a view layer. It does not contain business logic, it does not hold the AI provider key, and it does not hold the privileged database credential. It performs authentication, renders pages, and calls the backend on the user's behalf. Anything consequential happens server-side.
The backend is the only tier that holds the AI provider key and the privileged database credential. It is the only path to Claude. It enforces authentication, input validation, rate limiting, and the audit trail. Every consequential action a user takes flows through it.
The database is managed by Supabase. It enforces user isolation at the database level rather than relying on the backend to do it correctly on every query.
Authentication and session handling
Authentication is passwordless. The user enters their email; we send a one-click sign-in link; clicking it establishes the session. The same flow serves first-time and returning users. Identity is proven by demonstrated control of the email address. There is no password to phish, leak, reuse, or forget.
Sessions are issued by Supabase Auth as signed JSON Web Tokens using an asymmetric elliptic-curve signature. The backend verifies the signature on every request against the project's published public-key set. Tokens that are unsigned, signed with the wrong key, expired, malformed, or missing the user identifier are rejected before any application logic runs. Admin endpoints additionally require an admin role claim on the verified token.
Sessions live in httpOnly cookies; the session is not exposed to client-side JavaScript. The server refreshes the session on every request that traverses the edge layer, and the same edge layer redirects unauthenticated visits to gated routes back to the sign-in surface. Already-authenticated users hitting the sign-in surface are bounced into the product so they never see the form twice.
The destination users are returned to after sign-in is validated as a same-origin relative path at two layers, including after the magic-link click, so a hostile rewrite of the link cannot redirect users to a third-party site. Authentication-related logs record only failure flags and never include the user's email or the underlying provider error message.
Row-level data isolation
Every table in the database that holds user data has Row-Level Security (RLS) enabled with explicit policies. A user can only read, modify, or delete rows whose owner identifier matches their authenticated identity. Where a table is keyed by something other than the user identifier (for example, junction tables linking analyses and tags), the policy joins through the parent record's ownership.
This isolation is enforced by Postgres, not by application code. The authenticated Supabase client the backend uses on a user's behalf carries that user's claims; the database evaluates the policies on every query. A bug in the backend that omitted an explicit ownership filter would still be unable to reach another user's row.
There is a separate privileged credential the backend uses for narrow administrative tasks (writing the AI audit trail, aggregating the admin dashboard, processing account deletions). It is held only by the backend, never by the frontend, and its use is limited to operations that genuinely require bypassing the per-user policies.
Encryption in transit and at rest
All traffic between the browser, the frontend, the backend, the database, and the AI provider uses HTTPS. The frontend ships Strict-Transport-Security with a one-year horizon and subdomain coverage, so a browser that has visited Kindred once will refuse to downgrade to plain HTTP.
Data at rest in the managed Postgres database is encrypted at the volume level by the platform. There is no plain-text path from the browser to the database, and there is no plain-text storage of database content on disk.
We do not currently encrypt individual content fields at the column level above and beyond the volume-level encryption. Analysis text, AI prompts, and AI responses live inside the encrypted database, accessible only through authenticated and policy-bound paths.
AI provider key isolation
The Anthropic API key never leaves the backend runtime. It is not present in any frontend bundle, in any environment variable visible to the browser, in any source file, or in any committed configuration. It is read at startup from the backend's secret store and held in memory.
The frontend has no path to call Claude directly. Every AI call flows through a backend endpoint that authenticates the user, enforces rate limits, sanitizes input, and records an audit entry before the AI provider is invoked.
Input handling and rate limiting
The analysis endpoint enforces input checks at the boundary: empty input is rejected, length is bounded, control characters are removed, excessive whitespace is normalized, and URL-type inputs must match a well-formed HTTP(S) URL shape. All other request bodies are validated against typed schemas before any handler logic runs.
The analysis endpoint is also rate-limited per user with a sliding window. The limit is set high enough not to obstruct ordinary use and low enough to prevent abuse and cost runaway. State-changing endpoints additionally require authentication on every request.
We intentionally do not publish the exact rate-limit threshold here; that detail belongs in our internal engineering document, which we share with business customers on request. The same applies to other tuning parameters that primarily serve to inform someone probing for thresholds rather than a curious reader.
How a request flows
A typical analysis request follows the same path every time. The user submits a query in the explore interface. The frontend includes the current access token and sends the request to the backend. The backend assigns a trace identifier, verifies the token, checks the rate-limit window, sanitizes the input, and creates an analysis record (which the database immediately scopes to the authenticated user).
The six-phase pipeline runs in sequence. Each phase calls Claude through the same authenticated, audited path; each phase output is persisted and streamed to the client as it completes. Each AI call writes an audit row with the trace identifier, the phase, model identifier, token counts, latency, and cost. The trace identifier threads through every log line, every audit row, and the error envelope returned to the client, so support investigations can follow the identifier rather than user content.
Network controls and headers
The backend's cross-origin allow list is restricted to a known set of frontends. There is no wildcard. The HTTP methods and headers permitted by the API are restricted to what the product actually uses.
The frontend ships a strict set of HTTP response headers: a Content-Security-Policy that constrains where scripts, styles, images, fonts, and network connections can come from; X-Frame-Options DENY (the site cannot be framed); X-Content-Type-Options nosniff; a Referrer-Policy that does not leak full URLs across origins; a Permissions-Policy that denies camera, microphone, and geolocation; and Strict-Transport-Security as described above.
AI integration security
The AI provider is abstracted behind a single interface in the backend. Phase prompts are versioned, source-controlled files. The user's query is bound to a labeled slot in each phase prompt rather than concatenated into a free-form system prompt, which limits the surface for prompt-injection attempts at the construction step.
Each phase's structured output is parsed and shape-checked before it is persisted or returned to the user. Malformed phase output is surfaced as a phase error in the stream rather than silently passing through. Token counts and per-call cost are recorded on every output and every audit row.
Account and data deletion
When a user deletes their account from settings, the request requires an explicit "DELETE" confirmation, then runs a true hard delete. Preferences, tags, analyses (with their phase outputs and cross-links), AI audit log entries belonging to that account, and the Supabase auth user are removed.
The deletion is strictly scoped to the requesting user. Per-user data tables are cleared through the authenticated user's own credential, which means row-level security additionally bounds the delete to that user. The audit-log cleanup uses the backend's privileged credential and filters explicitly by the deleting user's identifier so that no other account's data can be affected by the delete.
Logging and audit
Every backend request gets a trace identifier of the form knd-<uuid> and a structured log entry that captures method, path, status code, duration, and a hashed user identifier. User identifiers are hashed before being placed in log lines, so log aggregation does not collect raw subjects.
Every AI call is recorded in an internal audit table with the trace identifier, the phase, the model, token counts, latency, and cost. This table is not user-facing; it is the audit trail that lets us investigate behavior, debug incidents, and account for spend honestly. The same record is what we delete when an account is deleted.
Secrets management
No secret is committed to the repository, ever. The repository's ignore file excludes every environment-file variant by default; the only environment files in version control are templates with no real values. Production secrets live in the deployment provider's secret stores, and the production AI provider key is held only in the backend runtime.
What is live today versus what is on our roadmap.
Every architectural item above is implemented in the product today. The items below are not. We list them because they are commonly expected at the next stage of company maturity, and we will not imply that they exist before they do.
Live today
- Three-tier separation with no privileged keys in the browser
- Supabase Auth with signed JWTs verified by the backend
- Row-level security on every user-data table, enforced by Postgres
- Encryption in transit (HTTPS, HSTS) and at rest
- AI provider key held only in the backend runtime
- Input validation, sanitization, and per-user rate limiting on the analysis path
- Strict CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS
- CORS locked to a known allow list (no wildcards)
- Trace-id-threaded structured logs with hashed user identifiers
- Append-only AI audit trail for every model call
- Account deletion as a true hard delete, scoped strictly to the deleting user
- Secrets excluded from version control by default
- Internal incident response plan covering categories, containment, notification (including GDPR's 72-hour timeline and US state obligations), recovery, and review
On our roadmap
- SOC 2 Type II readiness (no audit performed; the architecture is designed to support one)
- Third-party penetration testing (planned before public launch and at least annually thereafter)
- Automated dependency vulnerability scanning in CI (Dependabot or Snyk)
- Automated secrets scanning in CI and pre-commit (gitleaks or equivalent)
- Production observability hardening: error tracking, uptime monitoring, log aggregation, and alerting
- Rate limiter durability across multiple backend replicas (currently in-process)
- Tighter Content-Security-Policy (today's policy includes the Next.js runtime allowances)
- Hard cost ceiling on AI calls in addition to volume-based rate limiting
- Formal API key rotation cadence (the architecture supports rotation; the schedule is not yet automated)
- Cookieless, privacy-preserving analytics when analytics are introduced
Related pages.
For deeper engineering detail, we maintain an internal security document generated directly from the codebase. It carries an explicit "known gaps and roadmap" section and is reviewed on every security-relevant change. We are happy to share it with business customers under request.