Fellwork/Security/Security Policy

Information Security Policy

Version 1.0·Effective February 2026·Annual review cycle

1. Scope & Purpose

This document establishes the security policies, technical controls, and risk treatment decisions for the Fellwork web application and its supporting infrastructure.

In scope: Fellwork web application (app.fellwork.com, fellwork.com), Supabase PostgreSQL database, Vercel edge deployment, Meilisearch Cloud search indexes, Stripe payment integration, Planning Center Online OAuth integration, and Resend transactional email.

Out of scope: End-user devices and browsers; third-party service internal security.

2. Asset Inventory

Systems

AssetSensitivityNotes
PostgreSQL database (prod)CriticalLive user data — Supabase hosted
Web application runtimeHighNuxt 4 SSR on Vercel edge network
Payment processingCriticalStripe — no raw card data stored
PCO integrationHighOAuth tokens, org roster data
Search indexesMediumBible text only, no PII

3. Roles & Responsibilities

Access follows the principle of least privilege. End users can access only their own data, enforced at the database level by Row Level Security. The admin app role grants management UI access only — raw database access requires separate Supabase dashboard credentials. Server routes use the service role key only where RLS cannot be applied (webhooks, privileged writes).

4. Authentication & Access Control

User authentication

Authentication is provided by Supabase Auth. Supported methods: email + password (bcrypt) and Google OAuth. Sessions are JWTs stored in browser localStorage — no server-side session cookies.

Custom JWT claims

A Supabase Custom Access Token Hook injects user_role and is_suspended claims on every JWT issuance to accelerate RLS evaluation. The admin middleware always re-verifies role from the profiles table on every API request — JWT claims are not trusted alone for privilege decisions.

Route protection

All /app/* routes require authentication via a global middleware. An explicit allowlist permits unauthenticated access to onboarding, subscription, and invite flows. All /api/admin/* routes are protected by server middleware that verifies admin role from the database and checks suspension status before processing any request.

5. Data Classification & Storage

LevelExamplesStorage
CriticalAPI keys, OAuth tokens, Stripe customer IDsVercel env vars (server only)
ConfidentialNames, emails, study history, notesSupabase PostgreSQL with RLS
InternalPlugin configuration, grammar structureSupabase PostgreSQL
PublicBible text, grammar content, prophecy catalogSupabase + Meilisearch

6. Database Security

Row Level Security

RLS is enabled on all tables containing user data. Every query is scoped to the authenticated user at the database level — not just the application layer. Users structurally cannot read each other's data regardless of application bugs. Admin access goes through a separate authorize('admin') helper that reads JWT claims injected by the access token hook.

SQL injection prevention

All database queries use Supabase's PostgREST client, which uses parameterized queries exclusively. No raw SQL string concatenation exists in application code.

Migration governance

Database migrations are applied manually via the Supabase Dashboard SQL Editor or Management API. No automated push to production. Each migration is reviewed before application.

7. API & Web Service Security

Webhook security

Both Stripe and Planning Center webhook handlers verify cryptographic signatures before processing any payload. Stripe uses its official SDK (constructEvent) with timestamp validation to prevent replay attacks. PCO uses crypto.timingSafeEqual() for HMAC-SHA256 comparison to prevent timing attacks. Requests with invalid signatures are rejected with HTTP 400/401.

Input validation

User-controlled inputs are type-checked and sanitized before use. Numeric query parameters are clamped to safe ranges. All paginated endpoints enforce maximum result caps (200 records per page). OAuth state parameters are stripped of non-alphanumeric characters.

Admin endpoints

All /api/admin/* routes are protected by server middleware that verifies the user's admin role directly from the database on every request, independent of JWT claims. Suspended accounts are rejected regardless of role.

8. Secret & Credential Management

All secrets are stored as Vercel Environment Variables, accessible only to serverless functions, never included in the client-side JavaScript bundle. Production credentials are accessible only to engineers with Vercel production access.

The .env file is gitignored and must never be committed. Developers receive credentials through a secure channel. Secrets are rotated immediately on suspected compromise and annually at minimum for critical credentials (service role key).

9. Third-Party Integrations

ProviderRoleKey security property
SupabaseDatabase & authSOC 2 Type II; RLS enforced at DB level
VercelHosting & CDNSOC 2 Type II; DDoS protection
StripePaymentsPCI DSS Level 1; we are out of PCI scope
Planning CenterPCO integrationOAuth 2.0; tokens stored server-side only
ResendEmailSOC 2 Type II; email + content only
MeilisearchSearchPublic domain Bible text only; no PII

10. Cookie & Browser Storage Policy

Cookies

Fellwork uses one functional cookie: pco_oauth_state — a short-lived (10 minute maximum), HttpOnly, Secure, SameSite=Lax CSRF token set only when a user initiates a Planning Center OAuth connection. It is deleted immediately after the OAuth callback completes. No tracking cookies, advertising cookies, or persistent preference cookies are used.

Local storage

The Supabase JS client stores the authentication JWT in localStorage (standard practice). The user's color theme preference is also stored in localStorage to prevent flash of unstyled content, and is persisted to the user's profile server-side for authenticated users.

Do Not Track

Fellwork honors Do Not Track signals by default. No behavioral tracking, advertising, or user profiling exists in the application for any user. Vercel Analytics collects only anonymous, aggregated, cookieless edge metrics — individual users cannot be identified.

11. Incident Response

SeverityDefinitionResponse time
P1 CriticalActive breach, data exposure, service downImmediate (<1 hour)
P2 HighSuspected compromise, partial outage<4 hours
P3 MediumVulnerability discovered, degraded service<48 hours
P4 LowMinor issue, no immediate riskNext sprint

On a P1/P2 incident: (1) Contain — rotate compromised credentials immediately. (2) Assess — determine scope using Supabase audit logs and the admin audit log table. (3) Notify — GDPR requires supervisory authority notification within 72 hours for EU data subjects if personal data is exposed. (4) Remediate — patch and verify. (5) Document — written post-mortem for all P1/P2 incidents.

12. Risk Register & Treatment Plan

R-01

Service role key compromise

10 — CriticalMitigate

The database service role key bypasses RLS. If leaked, an attacker could read or modify all user data.

Controls: Stored only in Vercel environment variables. Not in git. Rotate annually or on suspected exposure.

R-02

SQL injection via user input

8 — HighMitigate

Malicious SQL in API inputs could read or modify data beyond the user's RLS scope.

Controls: All queries use parameterized PostgREST client. RLS provides defense-in-depth. No raw SQL concatenation.

R-03

Admin account takeover

8 — HighMitigate

A compromised admin account grants management access to user data and subscription controls.

Controls: Admin middleware re-verifies role from DB on every request. Suspension flag for immediate lockout. Audit log records all actions.

R-04

Preview deployment credential exposure

9 — CriticalMitigate

Vercel preview deployments could be publicly accessible with production environment variables.

Controls: Dev and prod use separate Supabase project branches. Preview deployments use dev variables only.

R-05

Supply chain compromise

8 — HighMitigate

A compromised npm package executes malicious code during build or runtime.

Controls: Lockfile pins exact versions. Vercel build environment is isolated. Dependabot alerts enabled.

R-06

Stripe webhook replay

6 — MediumMitigate

Replaying a captured webhook could trigger duplicate tier upgrades or billing events.

Controls: Stripe SDK validates HMAC signature and timestamp. Events older than 300 seconds are rejected.

R-07

Planning Center token exposure

6 — MediumMitigate

PCO OAuth tokens stored in the database are exposed to unauthorized users.

Controls: Server-side access only. RLS SELECT policy excludes token columns from client queries.

R-08

Missing schema validation

6 — MediumMitigate

Manual type checking rather than schema validation could allow unexpected input shapes.

Controls: Type checks on all user inputs. Result limits cap paginated endpoints. Zod adoption planned.

R-09

Silent webhook failures (PCO)

6 — MediumAccept

PCO webhook handler returns 200 on processing errors to prevent retry storms.

Controls: Processing errors are logged. Accepted trade-off to prevent exponential retry load.

R-10

Meilisearch key exposure

2 — LowAccept

The search API key is browser-accessible. Indexes contain only public domain Bible text.

Controls: Search-only key with no write permissions. No PII in search indexes. Low impact even on full exposure.

13. Compliance & Audit

GDPR

Legal basis: contract performance and legitimate interest. Users may request access, correction, or deletion of their data by contacting hello@fellwork.com. Account deletion cascades to all associated study data. The only functional cookie is strictly necessary and exempt from consent requirements under the ePrivacy Directive.

Audit log

The admin_audit_log table records all administrative actions with timestamp, actor ID, and affected resource. Reviewed quarterly and after any reported incident.

Security vulnerability reporting

Report vulnerabilities responsibly to security@fellwork.com. We aim to acknowledge within 48 hours and provide a remediation timeline within 5 business days.