* feat(notifications): add email notification preferences Implement the email notification preferences PRD with org-level defaults and personal opt-outs enforced at the email enqueue chokepoint. - Add toggle map, profile.settings jsonb, and shouldSendEmail resolver - Extend profile and organization update APIs with emailNotifications - Pass preference context through all toggleable email send call sites - Add Settings > Notifications page with personal and org admin sections Co-authored-by: Rotimi Best <[email protected]> * feat(notifications): group email toggles into course, cohort, and enrollment sections Organize personal and org notification settings into scannable sections so users can find course- or cohort-related emails quickly. Co-authored-by: Rotimi Best <[email protected]> * fix(notifications): use Exercise instead of Quiz in toggle copy Co-authored-by: Rotimi Best <[email protected]> * fix(notifications): only mark certification email sent when enqueued Skip setMemberCertificationEmailSent when notification preferences or missing Redis prevent the completion email from being queued, so the email can still send if preferences change later. Co-authored-by: Rotimi Best <[email protected]> * fix(notifications): address PR review feedback - Add EmailPreferenceLookupCache to avoid N+1 settings lookups in batch processors and multi-recipient email enqueue paths - Replace $effect store hydration in notifications settings with onMount and explicit sync after save - Remove thin api email-preferences re-export; import from db queries - Hide tutor-only personal toggles (newStudent, newSubmission) for students Co-authored-by: Rotimi Best <[email protected]> * fix(notifications): hydrate toggles after store load and add profile settings migration - One-time store hydration via onMount plus guarded effect when profile/org load after mount, without clobbering in-progress edits - Add drizzle migration for profile.settings jsonb column Co-authored-by: Rotimi Best <[email protected]> * feat(notifications): add email preferences to LMS settings Students manage personal email opt-outs from /lms/settings/notifications, matching the org settings page but scoped to the LMS navigation students use. Co-authored-by: Rotimi Best <[email protected]> * feat(notifications): store per-org personal prefs in organizationmember_email_notifications - Add organizationmember_email_notifications table keyed by organizationmember - Expose GET/PUT /organization/member/email-notifications for current org member - Resolve personal email prefs by profileId + organizationId at send time - Migrate legacy profile.settings.emailNotifications into per-membership rows - Update org and LMS settings UI to load/save org-scoped member preferences Co-authored-by: Rotimi Best <[email protected]> * fix(notifications): address PR review on locales and backfill script Move profile email-notification backfill out of the migration SQL into a dedicated db script with dry-run support. Translate personal preference description strings in all non-English locale files. Co-authored-by: Rotimi Best <[email protected]> * fix(i18n): translate notification preference strings in all locales Complete settings.notifications and settings.tabs.notifications_tab translations for da, de, es, fr, hi, pl, pt, ru, and vi. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
@cio/db
This package handles everything related to the database, schema, migrations and connection.
Stack
- Postgres - We use postgres for the database
- Drizzle ORM - For managing and querying the database
- Better Auth - For managing the auth tables (users, account, session).
Workflow
We used Supabase in the past for managing the supabase but have now moved on to use postgres directly and auth managed by our backend. This means the workflow is a mix of the Database First approach (Option 1) and the Codebase First approach (Option 2). Learn more about each approach from the drizzle migration docs.
Here is how I do it:
1. Local DB to Drizzle Schema TS file
I have a postgres tool where I can make changes to the db as I want. I am not so familiar with drizzle that much so I am sticking with this for now.
```bash
pnpm db pull # this does sql -> drizzle ts code
```
2. Drizzle Schema file to Prod SQL
Now when I am ready to deploy the feature, I can use option 2 which is makes the typescript schema the source of truth. This command will only run on CI on every merge on a condition that there was a change to the schema file packages/db/src/schema.ts
```bash
pnpm db push # this does drizzle ts code to prod db
```
OR
I can make changes directly to the schema file and run pnpm db push to push the changes to the source db.
Setting up a new DB on Railway
Once you setup a Postgres DB on Railway, you should copyt the DATABASE_PUBLIC_URL and paste into your .env. In the .env we expect DATABASE_URL:
```.env
DATABASE_URL=what-you-copied-from-railway
```
Commands
db:setup
Prepares the database so the application can run. Run this for new databases or after provisioning Postgres.
pnpm db:setup
It performs three steps in order:
- Schema sync – Runs
drizzle-kit pushto apply the TypeScript schema to the database (creates/updates tables, enums, etc.). - PostgreSQL roles – Creates the
authenticatedandanonroles if missing (used by RLS policies). - Essential seed data – Inserts required reference data: roles (ADMIN, TUTOR, STUDENT), submission statuses, and question types.
Optional: Add --seed (or -s) to run the full seed, including demo users, organizations, courses, and other example data.
Requires DATABASE_URL or PRIVATE_DATABASE_URL in the environment. The scripts load .env from this package directory, so bootstrap it once from the template:
cp .env.example .env # then set DATABASE_URL for your DB
db:backfill-profile-email-verification
Repairs stale profile.is_email_verified rows where the auth user is already verified (user.email_verified = true) or has a linked OAuth/SSO account, but the profile flag was never synced. Use after the auth/profile verification mismatch fix, or when users are blocked by the verify-email modal while send-verification-email returns EMAIL_IS_ALREADY_VERIFIED.
Dry run (lists affected count and a sample; makes no changes):
pnpm db:backfill-profile-email-verification
# or from repo root:
pnpm --filter @cio/db db:backfill-profile-email-verification
Apply updates:
pnpm db:backfill-profile-email-verification -- --execute
The script prints how many profiles were updated and verifies zero mismatches remain.
Seeding
The seed script populates the database with demo data for local development. It is idempotent — running it multiple times is safe; existing records are skipped.
Run all seeds
pnpm seed
# or explicitly
pnpm seed --all
Run individual seeds
Pass one or more flags to run only specific steps:
pnpm seed --users --profiles
pnpm seed --organizations --organization-members --organization-plan
| Flag | What it seeds |
|---|---|
--roles |
ADMIN, TUTOR, STUDENT roles |
--submissions |
Submission statuses |
--question-types |
Question types |
--users |
Auth users (from users.json) |
--profiles |
User profiles |
--accounts |
User accounts |
--organizations |
Demo organizations |
--organization-members |
Org membership records |
--organization-plan |
Organization plan records (enterprise + early adopter) |
--groups |
Course groups |
--group-members |
Group membership records |
--courses |
Demo courses |
--sections |
Course sections |
--lessons |
Lessons |
--exercises |
Exercises |
--questions |
Questions |
--templates |
Exercise templates |
Demo accounts
| Password | Role | Plan | |
|---|---|---|---|
[email protected] |
123456 |
Admin | Basic (free) |
[email protected] |
123456 |
Student | Basic (free) |
[email protected] |
123456 |
Admin | Enterprise |
[email protected] |
123456 |
Student | Enterprise |
[email protected] |
123456 |
Admin | Early Adopter |
[email protected] |
123456 |
Student | Early Adopter |
Mock user credentials are defined in users.json. The demo accounts above use the password 123456.