Trust, built into the schema.
Stock audits move a lender's most sensitive data between four parties. So we didn't bolt security on, we started there: isolation enforced by the database, encryption per tenant, presence proven by GPS.
The path of a single request. Every gate is a real control - the file it lives in is printed beneath it.
AES-0
GCM encryption at rest
0%
Requests scoped by RLS
0
Full Aadhaar numbers stored
0%
Scans carry GPS provenance
Security you can point to
No hand-waving. Each pillar maps to a concrete mechanism in the platform.
Row-level security, not app-level hope
Every request runs inside a transaction that sets request.jwt.claims and switches to the non-superuser authenticated role - so Postgres itself refuses to return another tenant's rows. It is the database enforcing isolation, not a WHERE clause an application bug could drop. An automated suite tries to breach it on every run.
Envelope encryption, keys held apart
Each organisation gets a random 256-bit data key. That key is stored only in wrapped form, sealed by a master key that lives in the environment and never in the database. Sensitive fields - inventory item names, addresses, signatory details - are AES-256-GCM ciphertext at rest. A stolen dump decrypts to nothing.
Identity verified against published keys
Access tokens are checked locally against Supabase's JWKS public keys, cached hourly, with an authoritative remote check as fallback. Crucially we never trust the role or tenant claimed inside a token: both are re-read from our own users table on every request, and deleted or deactivated accounts are refused.
Authorization checked on the server, every time
A role-to-permission matrix gates every API route through a single withAuth wrapper - permissions are never inferred from the client. Privileged actions are gated further: a tenant admin cannot assign or alter a super-admin, and cross-tenant writes are rejected even on the RLS-bypassing admin path.
Geo-fenced, tamper-evident counting
Every scan carries the device's GPS. The server computes great-circle distance from the store and applies the client's fence: hard mode voids an off-site scan outright, soft mode records and flags it for review. Reports are stamped with a SHA-256 hash over canonicalised content, so any later edit is detectable.
The count survives the device
Counting never waits for a network: scans are written to the device and sync themselves when signal returns. That local copy is also sealed - encrypted with a key the device holds only the public half of, so it can write the count and never read it back. A laptop lost in a godown leaks nothing, and a device that never reconnects can still be opened by an admin holding that audit's key. The copy destroys itself 24 hours after the audit closes.
Minimal-footprint identity data
Aadhaar verification validates the Verhoeff checksum, then stores only the last four digits and a one-way salted reference - never the number itself - with the signatory's name encrypted at rest. New passwords are screened against a common-weak denylist, and PII is never written to logs in the clear.
Eight controls, in the order a request meets them
Each is a specific mechanism in a specific file, not a policy statement. A request clears every layer before it reaches your data.
next.config.tsHSTS (1 year, subdomains), nosniff, SAMEORIGIN, strict-origin-when-cross-origin, and a Permissions-Policy granting only camera and geolocation - the two the scan station needs.
middleware.tsEdge middleware refreshes the Supabase session on navigation, so auth cookies rotate before a Server Component ever reads them.
server/rate-limit.tsPer-IP fixed-window rate limits on every auth route: 10 logins a minute, 5 registrations per 5 minutes, 5 join and bootstrap attempts per 10 minutes.
server/jwt.tsToken signatures are verified against the project's published JWKS public keys - asymmetric, so a leaked server secret cannot mint a valid token. The cookie session is verified server-side. Neither path trusts client-supplied claims.
server/permissions.tsA single withAuth wrapper discards whatever the token claims and re-reads the caller's role and tenant from the database, then checks the route's required permission against the role matrix before the handler runs.
@repo/validationEvery request body is parsed by a Zod schema from the shared validation package - malformed or unexpected input never reaches a service.
packages/db/rls.tsThe service runs inside a transaction under the authenticated role with the caller's claims set, so Postgres RLS policies apply to every read and write.
lib/offline/vault.tsEvery scan is also written to an encrypted store in the browser, sealed with the public key of that one client, store and audit. The device can write it and never read it back, so a stolen laptop is worthless - and a count survives a device that never finds a network again.
server/envelope.tsSensitive columns are sealed with the organisation's own data key before they are written. That key is itself stored wrapped, and the master key that unwraps it lives outside the database entirely.
Asserted is not the same as verified
The claims above are covered by automated tests that run on every change: a suite that attempts a cross-tenant read and asserts Postgres refuses it, a permission-matrix suite that asserts each role reaches only its own routes, a canonicalisation suite proving two logically identical reports hash identically, and a duplicate-scan suite for the counting pipeline. Isolation is a test that fails loudly, not a sentence on a marketing page.