Thalix VaultDocumentation
Browse documentation
Architecture
publishedimplementedEdit this page

Trust boundaries (as implemented)

This page documents the concrete database and transport controls behind Architecture overview's control-plane summary, each grounded in the actual migration or source file that enforces it.

Row Level Security plus a full privilege revoke

Every application table has Row Level Security enabled, and every client-facing database role (anon, authenticated) has every ordinary table privilege (select/insert/update/delete) revoked outright:

alter table public.files enable row level security;
revoke all on public.files from public, anon, authenticated;

This is deliberately belt-and-braces: RLS alone would still require a correct policy on every table and every command to fail closed; revoking table privileges entirely means there is no row-level policy to get wrong in the first place for ordinary client roles, because those roles cannot touch the table at all except through a function that has been explicitly granted execute.

SECURITY DEFINER RPCs are the only mutation path

Every read and write goes through a SECURITY DEFINER PL/pgSQL function (set search_path = '' to prevent search-path hijacking), granted to service_role or anon explicitly per function, never to authenticated directly for application tables. A function performs authentication (resolving a tv_ bearer token or a Supabase session), authorization (resolve_file_access, organization role checks), input validation, and mutation together in one call — there is no separate "check access, then act" round trip a race condition could split. admin_audit_list is a representative example: it is the sole SECURITY DEFINER surface over audit_events_view, so authorization and keyset pagination happen exactly once, in SQL.

Service-role isolation

The Node application server holds the Supabase service role key; it is never sent to the browser or the CLI. authenticate_cli_access_token and its callers accept a hash of the CLI's bearer token (sha256, hex), never the raw token — so a database-level compromise that leaks query logs still cannot recover a usable credential. Two migration decisions push this further:

  • Purge is structurally forbidden. files and file_versions carry BEFORE DELETE (and, for a committed version, BEFORE UPDATE) triggers that unconditionally raise a custom SQLSTATE ('TVPRG') for every role, including service_role. This matters specifically because Supabase's service_role bypasses RLS and ordinary privilege grants by default — a trigger has no such bypass, so this is the one control that holds even if the service-role credential itself were ever misused. file_metadata_revisions and every audit table carry the equivalent append-only trigger. No RPC, HTTP route, or CLI command anywhere in this codebase ever issues a DELETE against any of these tables, and a client-shaped attempt at the HTTP layer (an HTTP DELETE against /api/internal/files/*) is already refused with 405 Method Not Allowed before any RPC runs.
  • Append-only audit is a database guarantee. The same trigger pattern applies to every audit table feeding audit_events_view. tv admin audit list can filter and page; nothing in tv, and no administrator override, can edit or remove an event.

Signed cookies vs. bearer tokens — two separate session mechanisms, by design

Thalix Vault deliberately runs two independent session mechanisms that never share storage:

SurfaceCredentialStorage
Browser (login, account, device approval)Supabase access + refresh tokenSecure; HttpOnly; SameSite=Lax cookies, server-set only
CLI (tv)Opaque tv_ device token, account:read scopeA private, permission-locked local file (see Security and authorization)

The CLI never receives, stores, or transmits a Supabase access or refresh token — it only ever holds the CLI-specific opaque token issued through the device-approval handshake, and that handshake is validated in the browser using the browser's own HttpOnly cookie session, never by handing the Supabase session itself to the CLI. This means a stolen tv_ token cannot be used to sign in to the web app, and a stolen browser cookie cannot be used as a tv credential — the two blast radii are disjoint by construction, not by convention.

The release trust root

Standalone tv binaries verify every release against an Ed25519 public key embedded directly in CLI source (packages/cli/src/release/trust.ts's EMBEDDED_RELEASE_TRUST_ROOTS), not a key fetched at runtime — so a compromised distribution channel (a hijacked thalix-vault-releases repository, a DNS attack, a malicious mirror) cannot make tv update trust a forged signature; the trust anchor never leaves the binary that ships it. The active production key is a563c039672742d7. Rotation appends a new entry with status: "active" and flips the old entry to status: "retired" — retired keys are kept forever (never deleted) so manifests signed before a rotation keep verifying, while a freshly compiled binary only trusts currently-active entries. A test-only override (TV_RELEASE_TEST_TRUST_ROOT) exists purely for the fixture-server test suite: a real compiled standalone binary (BUILD_KIND === "standalone") hard-refuses with RELEASE_TEST_OVERRIDE_FORBIDDEN if that environment variable is ever set, so no environment variable can talk a real installation into trusting an attacker-supplied key.

Continue with Security and authorization for the role/grant model this infrastructure enforces, or Current state for what remains outside these guarantees today (unsigned Windows builds, unnotarized macOS builds, a single Supabase data plane).