Security
Auth model, data collected, GDPR/CCPA posture, deletion, webhook integrity
Auth model
WorkOS AuthKit is the identity provider for the mobile app + web app. The docs site (this site) is public — no auth. Both authenticated surfaces share one WorkOS tenant.
- Sign-in methods: email + password, magic link, Google OAuth (mobile only today).
- Sessions: cookie-based on web, JWT-via-secure-store on mobile.
- Token storage on mobile:
expo-secure-store— Keychain on iOS, EncryptedSharedPreferences on Android. - Token storage on web: WorkOS AuthKit's encrypted session cookie (
WORKOS_COOKIE_PASSWORDencrypts contents). - Passwords: NXT does not store them. WorkOS does.
How Convex trusts the session
convex/auth.config.ts registers WorkOS as the identity provider. Convex's userQuery / userMutation / userAction wrapped factories pre-resolve ctx.user from the verified session. Raw query() / mutation() is forbidden by lint — they are publicly callable.
Authorization
Every query that takes a document ID checks ownership before returning:
const doc = await ctx.db.get(id);
if (!doc || doc.userId !== ctx.user._id) throw forbidden();Forgetting this check = IDOR (insecure direct object reference). The error-factory pattern + the wrapped factories + code review enforce.
Org-scoped resources
Where a resource is shared within an organization (the b2b addon's territory; not actively used by NXT today), the same pattern applies with orgId instead of userId. NXT today is consumer-only, so most checks are user-scoped.
What student data is collected
| Category | Field | Source | Purpose |
|---|---|---|---|
| Identity | email, WorkOS id | WorkOS signup | account |
| Academic | GPA, SAT, ACT, AP/IB | profile form | RFS, matching |
| Background | race, gender, first-gen status | profile form | MSI rails, peer outcomes |
| Finance | family income bracket (1 of 5 federal) | profile form | Afford peek, net-price match |
| Interests | study areas (8 categories) | quiz | Picked For You, Program Leader rails |
| Personality | quiz result (one of N personas) | quiz | Learning Style rail |
| Campus setting | rural/town/suburb/metro, size, walkability, politics | quiz | Campus Vibe rail |
| Story | free text (achievements, extracurriculars, community service, personal story) | profile form | profile depth signal |
| Saves | college unitId + timestamp + RFS verdict at save | save action | the user's college list |
| Quizzes | per-question answers | quiz | derive personality + setting |
No financial transactions. No payment data. NXT does not bill students.
No conversations. No chat with counselors or peers. No DMs. No messaging at all.
No geo-tracking. Location is the user's self-reported home city/state, never GPS-tracked.
Where data lives
| Table | What | Encryption at rest |
|---|---|---|
users | account record, WorkOS id | Convex-managed |
profiles | structured profile fields | Convex-managed |
quiz* | quiz answers | Convex-managed |
savedSchools | saves | Convex-managed |
rfsVerdicts / collegeReasoning | derived data, cached | Convex-managed |
| WorkOS | password hashes, MFA factors, OAuth tokens | WorkOS-managed |
Convex encrypts data at rest. WorkOS encrypts credentials. No NXT-controlled key escrow.
Webhook integrity
Incoming webhooks (WorkOS user-creation, etc.) are verified by HMAC signature using WORKOS_WEBHOOK_SECRET. The webhook_events table is an idempotency ledger keyed by event ID — duplicate deliveries are no-ops.
If WORKOS_WEBHOOK_SECRET is unset, webhooks are not verified. Today the secret is set.
Account deletion
User-initiated. Triggered from the mobile app (settings) or the web account-deletion page (apps/web/app/(marketing)/delete-account/page.tsx).
The deletion path is a workflow (@convex-dev/workflow):
- Mark the user as
deletion_pendinginusers. - Cascade-delete user-scoped rows:
profiles,quiz*,savedSchools,rfsVerdicts,collegeReasoning,reports. - Notify WorkOS to delete the underlying identity (idempotent — WorkOS is the source of truth for the auth record).
- Notify PostHog via the GDPR erasure path (
/api/erase/<distinct_id>) usingPOSTHOG_PERSONAL_API_KEY. - Mark
usersrow hard-deleted.
The workflow is restartable. If step 3 fails, the workflow retries until WorkOS confirms; the user row stays in deletion_pending until fully cascaded.
Race protection. If a user signs back in mid-deletion, the auth path checks deletion_pending and refuses session creation. No accidental zombie-account.
GDPR / CCPA posture
- Right to access. A user can download their profile data via the mobile settings page (export-as-JSON action wired but UI flow shallow today).
- Right to erasure. The account-deletion cascade above. PostHog gets a separate erasure call via API.
- Data residency. Convex deployment region is configurable. Today: US-East. WorkOS region: US. PostHog region: EU (hosted EU cloud).
- DPA / SCC. Not signed at the entity level today. If NXT signs an enterprise customer that requires a DPA, the trio of (Convex, WorkOS, PostHog) all offer one.
Content reports
The reports table accepts abuse reports from inside the app (user reports a school, a piece of content, or another user — though users-reporting-users is not a UI flow today since there's no chat).
REPORT_ADMIN_EMAIL(optional) is notified on new reports.- The dedup query (
reports/mutations.ts) filters on(reporterId, _creationTime)over the target's recent reports and takes.first()to prevent spam.
App Store / Play Store reviewer access
REVIEW_LOGIN_EMAILS env var holds the test accounts the App Store and Play Store reviewer teams use. These accounts have full profile data + saves so reviewers can exercise the matching engine without setting up their own.
Don't share these credentials outside the App Store / Play Store flow.
What's NOT in scope
- HIPAA / PCI / SOC 2. Not pursued. No PHI, no payment data, no enterprise compliance customer yet.
- Pen-test cadence. No formal pen-test scheduled. Recommended once the user base passes 10K MAU or an enterprise customer with security requirements signs.
- CSP / SRI on web. Not configured today. Marketing-only surface; risk is low.
- Mobile binary protection / RASP. Not configured. Standard Hermes JS bundle + Expo bridge.
Risks
- Single-secret blast radius. Leaking
WORKOS_API_KEYorOPENAI_API_KEYis bad but bounded. Mitigation: org-level cost limit on OpenAI, WorkOS audit log monitoring (manual). - No secret rotation policy. Open work item — see Credentials.
- Mobile dev client allows arbitrary JS. Don't push a dev-client build to production.
- No 2FA enforced on Convex / Vercel / GitHub / WorkOS accounts. Recommended to enable.
Technical detail
Why no end-to-end encryption
Users' profile data is not confidential to NXT — the entire product is built on the user trading data for matches. E2EE would prevent the matching engine from working. The threat model is hostile-actor-accessing-server, not NXT-itself-as-adversary; Convex's at-rest encryption + the auth model address that.
Why JWT on mobile, cookie on web
Browsers handle HTTP-only cookies safely; React Native does not have a cookie jar by default. WorkOS AuthKit's mobile SDK uses JWT in Secure Storage as the equivalent.
Why webhook signing matters
WorkOS can also be misconfigured to fire a webhook to your endpoint with a forged event payload. HMAC + the webhook_events ledger means a forged event either fails signature check (rejected) or replays an existing event ID (idempotent no-op).