Security documentation for the Intake Tracker health PWA. This is a reference document describing implemented patterns, not aspirational goals.
Single-user health tracking PWA. All user data lives client-side in IndexedDB. The security model prioritizes access control over encryption at rest, since IndexedDB does not support transparent encryption and the app relies on useLiveQuery reactivity which requires plaintext-queryable data.
- Provider: Neon Auth (email/password) backed by Neon Postgres
- Whitelist enforcement:
ALLOWED_EMAILSenv var checked server-side insrc/lib/auth-middleware.ts - Session verification: HttpOnly session cookie validated on every API request via
withAuth()insrc/lib/auth-middleware.ts - Client guard:
src/components/auth-guard.tsxwraps protected routes
E2E tests use a seeded Neon Auth user (email + password) for automated authentication. Playwright's globalSetup (e2e/global-setup.ts) authenticates against Neon Auth, captures the session cookie, and persists playwright/.auth/user.json for all test projects to reuse.
All API keys are server-side only. No secrets are exposed via NEXT_PUBLIC_ environment variables.
ANTHROPIC_API_KEY-- used in API routes only (src/app/api/ai/_shared/claude-client.tsshared by all AI routes)NEON_AUTH_COOKIE_SECRET-- used by Neon Auth to sign session cookiesNEON_AUTH_URL-- Neon Auth service endpoint (proxied via/api/auth/[...path])DATABASE_URL-- Neon Postgres connection string (Neon Auth user store + push notification tables)
API routes use auth middleware from src/lib/auth-middleware.ts to validate the Neon Auth session cookie and check the email whitelist before processing requests.
sanitizeForAI() in src/lib/security.ts strips emails, phone numbers, and SSN patterns from text before sending to the AI API. Input is also length-limited to 500 characters.
The app does not encrypt user data itself. There is no PIN-based encryption, no field-level encryption and no encrypted backup format.
src/lib/backup-service.ts exports and imports plain JSON backups. A
backup file holds the user's health and medication records in clear text;
keep it somewhere only the user can read. importBackup(file) refuses a file
marked encrypted: true (a format from an earlier, never-exposed code path)
with a clear error instead of importing it.
The earlier exportEncryptedBackup / importEncryptedBackup functions and
their AES-GCM/PBKDF2 helpers (src/lib/crypto.ts) were removed: nothing in
the UI ever called them.
Sync (/api/sync/*) and every other API call run over HTTPS. In cloud-sync
mode the records are also stored server-side in Neon Postgres; the app adds
no encryption of its own there.
- IndexedDB is NOT encrypted. All record data is stored in plaintext in the browser's IndexedDB. This is a deliberate tradeoff:
useLiveQueryrequires direct IndexedDB queries, which are incompatible with transparent encryption. - Access control is the primary protection. Neon Auth prevents unauthorized access to the app.
- No field-level encryption. No record field is encrypted on the device.
Defined in next.config.js and applied to all routes via response headers:
| Directive | Value | Reason |
|---|---|---|
default-src |
'self' |
Restrict all resources to same origin |
script-src |
'self' 'unsafe-eval' 'unsafe-inline' |
Required for Next.js |
style-src |
'self' 'unsafe-inline' |
Required for Tailwind CSS |
img-src |
'self' data: blob: |
App-generated images |
font-src |
'self' data: |
Outfit font via next/font |
connect-src |
'self' https://api.anthropic.com https://*.neon.tech |
Anthropic Claude API + Neon Auth |
frame-src |
'self' |
No third-party iframes |
frame-ancestors |
'none' |
Prevent embedding (clickjacking) |
Additional headers: X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy: camera=(), microphone=(self), geolocation=().
The auth patterns are designed for future server-side sync:
- Cookie session verification: Already implemented server-side via Neon Auth, ready for API-based sync endpoints
- No Dexie Cloud dependency: The app uses plain Dexie.js, not Dexie Cloud, so sync can be implemented via any backend
- Schema has realmId fields: Database records include
realmIdfor future multi-tenant partitioning when server-side sync is added - Device tracking: Records include
deviceIdfor conflict resolution across devices
| Variable | Secret | Location | Purpose |
|---|---|---|---|
NEON_AUTH_COOKIE_SECRET |
Secret | Server only | Neon Auth session cookie signing (≥32 chars) |
NEON_AUTH_URL |
Safe | Server only | Neon Auth service endpoint (proxied via /api/auth/[...path]) |
DATABASE_URL |
Secret | Server only | Neon Postgres connection (Neon Auth + push tables) |
ANTHROPIC_API_KEY |
Secret | Server only | AI parse/search/enrich API |
ALLOWED_EMAILS |
Safe | Server only | Email whitelist for auth |
NEXT_PUBLIC_APP_VERSION |
Safe | Client | Display version (from package.json) |
NEXT_PUBLIC_GIT_SHA |
Safe | Client | Display git SHA |
NEXT_PUBLIC_VERCEL_ENV |
Safe | Client + Server | Environment detection |
VERCEL_GIT_COMMIT_SHA |
Safe | Server (Vercel) | Git SHA source |
VERCEL_ENV |
Safe | Server (Vercel) | Environment detection (production/preview/development) |