Last reviewed: June 12, 2026
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.
Above and beyond that volume-level encryption, the sensitive user content is encrypted by the application itself, at the field level, before it is written. The generated analysis content, the user's question, and the analysis title are stored as authenticated AES-256-GCM envelope ciphertext: each value is sealed with its own single-use data key, and that data key is wrapped by a versioned master key that lives only in the backend environment, never in the database. A stolen database dump, or a leaked database credential used to read the database directly, reaches ciphertext for these fields, not their contents, unless the separate master key is also obtained. The backend decrypts on read to serve the owner's library and to run new analyses; the envelope design lets the master key be rotated without re-encrypting content.
The honest boundary: this is encryption at rest, not end-to-end encryption. To produce an analysis, the backend and the AI provider necessarily process the question in readable form, so this protects stored data against theft of the database or its credentials, not against a fully compromised running backend. The same field-level encryption applies to the stored bring-your-own-key secret.
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 Ask 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 analysis itself runs as a detached job on the server. Submission starts the run and returns immediately; the client then watches by polling the persisted state. No live connection is load-bearing: closing the laptop, losing the network, or navigating away does not affect the run, and a signed-in user can re-enter an in-progress analysis from any device.
The five reasoning phases and the Sources & References record are produced by that job, and each phase output is persisted the moment it completes, so what the client shows is always read back from durable state. Each phase calls Claude through the same authenticated, audited path. 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.
A finished run carries an honest status. Completed means every phase succeeded. Partial means some phases succeeded and the rest are shown as calm per-phase notices rather than hidden. Failed means nothing usable was produced. Cancelled means the user deliberately stopped the run, which they can do while it is in progress. A background reconciler detects runs orphaned by infrastructure interruptions and marks them honestly rather than leaving them appearing active.
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 validated against an explicit schema before it is persisted or returned to the user. Output that fails validation is retried once with explicit error context; if it still fails, that phase is persisted as an honest per-phase error rather than silently passing through, and the rest of the analysis continues. Token counts and per-call cost are recorded on every output and every audit row.
The reference list at the end of every analysis is grounded by construction. It is built only from sources actually retrieved by web search during that analysis; any reference whose URL is not in that retrieved pool is removed before the user sees it, and every removal is escalated for operator review rather than silently discarded.
Inline citations follow the same discipline. The models cite the exact URL of a retrieved source, and the engine resolves each citation to the actual source in the retrieved pool, so a resolved citation opens the real document the claim drew on. One honest limit, stated plainly: citations link claims to real retrieved sources, but the content of each claim is not yet mechanically verified against the source. That verification is on the roadmap.
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, cross-links, and source records), the encrypted bring-your-own-key secret if one was stored, and the operational and audit records tied to that account (AI call traces, phase-attempt records, and usage events) are all removed, along with the Supabase auth user. Nothing carrying the account's content or identifier is left behind.
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 records that have no user-facing access path (AI call traces, phase-attempt records, usage events, and the key secret) are cleared with the backend's privileged credential, filtered 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), at rest (volume-level), AND application-level field encryption of user content (analysis content, question, and title) with AES-256-GCM envelope encryption
- 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)
- Analyses as detached server-side jobs: phase outputs persist as they complete, runs survive disconnects, and terminal statuses are honest (completed, partial, failed, cancelled)
- Background reconciliation that marks runs orphaned by infrastructure interruptions instead of leaving them appearing active
- Reference lists grounded by construction: built only from sources retrieved during the analysis, with removals escalated for operator review
- Inline citations resolved to the actual retrieved source
- 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
- Mechanical verification of each cited claim's content against its source (citations link to real retrieved sources today; content verification is not yet built)
- 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.