Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ Run with `pnpm <script>`.
**Setup**
- `setup` — Full setup (install, hooks, gitleaks, osv, database)
- `setup:gitleaks`, `setup:osv` — Install Gitleaks, OSV scanner
- `setup:database` — Database tools
- `setup:database` — Database tools (Docker, Supabase CLI)
- `reset` — Local API database: Supabase reset + Drizzle migrations + seed (`pnpm --filter @repo/api reset`). See [apps/api/README.md](apps/api/README.md)

**Primary**
- `build` — Build packages and apps
- `dev` — Start dev (core, react, error, utils, api, web)
Expand Down
9 changes: 6 additions & 3 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Type-safe REST API built with Fastify & OpenAPI. Routes in `src/routes/` are the

Copy [`.env.defaults.example`](.env.defaults.example) to `.env` and set values (gitignored). Start database first (`pnpm db:start`), then `pnpm dev`. Uses Supabase CLI for PostgreSQL, or `PGLITE=true` for in-memory. Dev server at [http://localhost:3000](http://localhost:3000).

**Switching project_id:** If you change `project_id` in `supabase/config.toml` (e.g. after a rebrand), run `pnpm db:stop` before `pnpm db:start`—only one Supabase instance runs per host.

## Vercel

Uses `framework: "fastify"` in vercel.json. Vercel auto-detects `server.ts` as the entrypoint. PostgreSQL migrations run at build time; PGLite migrations run at runtime.
Expand All @@ -30,14 +32,15 @@ Copy `.env.test.example` to `.env.test` (gitignored) for unit tests. Vitest load
- `pnpm test:e2e:debug` — Debug E2E tests
- `pnpm checktypes` — Type-check
- `pnpm db:start` — Start Supabase (local)
- `pnpm db:stop` — Stop Supabase
- `pnpm db:reset` — Reset Supabase database (recreates from scratch)
- `pnpm db:reset-and-migrate` — Reset DB then run Drizzle migrations
- `pnpm db:stop` — Stop Supabase (run before switching to another project’s Supabase)
- `pnpm reset` — From repo root: `pnpm --filter @repo/api reset`. From `apps/api`: Supabase DB reset, then Drizzle migrations (`scripts/migrate.ts`), then seed (`scripts/seed.ts`) with local `DATABASE_URL` + `RUN_PG_MIGRATE=true`. `[db.seed]` / `seed.sql` unused (`supabase/config.toml`)
- `pnpm db:migrate` — Run migrations (skips when PGLITE=true; use `RUN_PG_MIGRATE=true` to force PostgreSQL)
- `pnpm db:generate` — Generate migrations from schema
- `pnpm db:push` — Push schema (dev only)
- `pnpm generate:openapi` — Regenerate OpenAPI spec

**Database:** `drizzle.config.ts` defines schema glob (`src/db/schema/tables/*.ts`) and migration output (`src/db/migrations`). `scripts/migrate.ts` runs the Drizzle migrator against PostgreSQL (or skips at build time when using PGLite—see `src/db/migrate.ts` at runtime). `pnpm reset` runs `scripts/seed.ts` (`runSeed`) after migrations; `pnpm db:migrate` alone does not.

## Links

- [Environment setup](https://basilic-docs.vercel.app/docs/development) — Env vars, `DATABASE_URL`, `PGLITE`
Expand Down
1 change: 1 addition & 0 deletions apps/api/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Migrations + schema. Local data seed: `scripts/seed.ts` (via `pnpm reset` in apps/api). */
import 'dotenv/config'
import { defineConfig } from 'drizzle-kit'

Expand Down
3 changes: 1 addition & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
"generate:openapi": "node --import tsx scripts/generate-openapi.ts",
"db:start": "supabase start",
"db:stop": "supabase stop",
"db:reset": "supabase db reset",
"db:reset-and-migrate": "pnpm db:reset && RUN_PG_MIGRATE=true DATABASE_URL=\"postgresql://postgres:postgres@127.0.0.1:54322/postgres\" pnpm db:migrate",
"reset": "supabase db reset && RUN_PG_MIGRATE=true DATABASE_URL=\"postgresql://postgres:postgres@127.0.0.1:54322/postgres\" sh -c 'pnpm db:migrate && node --import tsx scripts/seed.ts'",
"db:status": "supabase status",
"db:migrate": "node --import tsx scripts/migrate.ts",
"db:generate": "NODE_OPTIONS='--import tsx' drizzle-kit generate",
Expand Down
8 changes: 4 additions & 4 deletions apps/api/scripts/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async function initMigrationsTrackingWhenTablesExist(

try {
// --- PGLite vs PostgreSQL ---
// Allow explicit override: RUN_PG_MIGRATE=true forces PostgreSQL path (e.g. after db:reset)
// Allow explicit override: RUN_PG_MIGRATE=true forces PostgreSQL path (e.g. after pnpm reset)
const forcePg = process.env.RUN_PG_MIGRATE === 'true'
const shouldUsePGLite = !forcePg && (env.PGLITE === true || env.NODE_ENV === 'test')

Expand Down Expand Up @@ -102,7 +102,7 @@ try {
migrationsTableExists = migrationsTableCheck.rows[0]?.exists ?? false

if (!migrationsTableExists) {
// Tables that must exist to consider this a "seeded" DB (matches initial schema)
// Tables that must exist to treat the DB as already matching the initial schema (migration bootstrap)
const requiredTables = ['users', 'sessions', 'verification', 'account', 'wallet_identities']
const tableCheckPromises = requiredTables.map(tableName =>
pool.query(
Expand Down Expand Up @@ -162,15 +162,15 @@ try {
} else if (isTableExistsError) {
logger.warn(
{ context: 'migrate', err: migrationError },
'Migration failed due to existing tables. Tables appear to match schema. For clean state, run: pnpm --filter @repo/api db:reset',
'Migration failed due to existing tables. Tables appear to match schema. For clean state, run: pnpm --filter @repo/api reset',
)
// In development/build, allow this to pass if tables exist and match schema
// In production, this should fail to ensure proper migration tracking
if (process.env.NODE_ENV === 'production') throw migrationError

logger.info(
{ context: 'migrate' },
'Allowing build to continue - tables exist and appear to match expected schema. Consider running db:reset for clean migration state.',
'Allowing build to continue - tables exist and appear to match expected schema. Consider running pnpm reset for clean migration state.',
)
} else {
logger.error({ context: 'migrate', err: migrationError }, 'Migration failed')
Expand Down
61 changes: 61 additions & 0 deletions apps/api/scripts/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* Data seed after a local Supabase reset. Invoked only from `pnpm reset` (apps/api or repo root via filter).
* (not from `pnpm db:migrate` / `pnpm build`).
*
* Add idempotent inserts here (`onConflictDoNothing()` / upserts). Uses the same
* PostgreSQL vs PGLite rules as `scripts/migrate.ts` (`RUN_PG_MIGRATE`, `DATABASE_URL`).
*/
import 'dotenv/config'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { logger } from '@repo/utils/logger/server'
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from '../src/db/schema/index.js'
import { env } from '../src/lib/env.js'

const scriptFile = fileURLToPath(import.meta.url)

async function applySeed(_db: NodePgDatabase<typeof schema>): Promise<void> {
// Reference / dev rows only — extend with db.insert(...).onConflictDoNothing(), etc.
}

export async function runSeed(): Promise<void> {
const forcePg = process.env.RUN_PG_MIGRATE === 'true'
const shouldUsePGLite = !forcePg && (env.PGLITE === true || env.NODE_ENV === 'test')

if (shouldUsePGLite) {
logger.info({ context: 'seed' }, 'PGLite: skipping data seed')
return
}

if (!env.DATABASE_URL) throw new Error('DATABASE_URL is required when PGLITE is false')

const pool = new Pool({ connectionString: env.DATABASE_URL })
const db = drizzle(pool, { schema })
try {
await applySeed(db)
logger.info({ context: 'seed' }, 'Seed completed')
} finally {
await pool.end()
}
}

function isMainModule(): boolean {
const entry = process.argv[1]
if (!entry) return false
return resolve(entry) === scriptFile
}

async function main(): Promise<void> {
try {
await runSeed()
process.exit(0)
} catch (err) {
logger.error({ context: 'seed', err }, 'Seed failed')
process.exit(1)
}
}

if (isMainModule()) void main()
3 changes: 2 additions & 1 deletion apps/api/src/routes/reference/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export function getReferenceHtml(opts: {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Reference - Basilic</title>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest/dist/browser/standalone.js"></script>
<!-- Pin matches @scalar/fastify-api-reference; avoids @latest hash/URL drift in E2E -->
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.44.18/dist/browser/standalone.js"></script>
<style>${scalarStyles}
</style>
</head>
Expand Down
8 changes: 3 additions & 5 deletions apps/api/supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "basilic-fastify"
project_id = "basilic-api"

[api]
enabled = true
Expand Down Expand Up @@ -59,10 +59,8 @@ schema_paths = []

[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
# Disabled — schema from Drizzle (scripts/migrate.ts); optional data rows from scripts/seed.ts via pnpm reset (apps/api)
enabled = false

[db.network_restrictions]
# Enable management of network restrictions.
Expand Down
39 changes: 32 additions & 7 deletions apps/api/test/swagger-login.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,27 @@ async function extractMagicLinkData(
}
}

/** Magic-link callback: replaceState clears token query; Scalar may add a hash — avoid strict full-URL match. */
async function waitForReferenceAuthSettled(
page: ReturnType<typeof test>['page'],
{ timeoutMs = 20_000 } = {},
) {
await expect
.poll(
async () =>
page.evaluate(() => {
const u = new URL(window.location.href)
return (
Boolean(localStorage.getItem('scalar-token')) &&
u.pathname === '/reference' &&
!u.searchParams.has('token')
)
}),
{ timeout: timeoutMs },
)
.toBe(true)
}

test.describe('Scalar UI Login Flow', () => {
test.describe.configure({ mode: 'serial' })

Expand Down Expand Up @@ -63,12 +84,14 @@ test.describe('Scalar UI Login Flow', () => {
if (!magicLink) throw new Error('Failed to extract magic link token and verificationId')

// Step 9: Open callback URL with token+verificationId (server verifies and returns HTML with JWT)
const callbackUrl = `${apiUrl}/reference?token=${magicLink.token}&verificationId=${magicLink.verificationId}`
await page.goto(callbackUrl)
const callbackUrl = new URL(`${apiUrl}/reference`)
callbackUrl.searchParams.set('token', magicLink.token)
callbackUrl.searchParams.set('verificationId', magicLink.verificationId)
await page.goto(callbackUrl.toString())
await page.waitForLoadState('networkidle')

// Step 10: Wait for callback page to process and clean URL (template does history.replaceState)
await page.waitForURL(/\/reference$/, { timeout: 15000 })
// Step 10: Wait for callback script (replaceState + localStorage); allow Scalar hash on URL
await waitForReferenceAuthSettled(page)

// Step 11: Check that token is stored in localStorage
const tokenInStorage = await page.evaluate(() => localStorage.getItem('scalar-token'))
Expand Down Expand Up @@ -119,9 +142,11 @@ test.describe('Scalar UI Login Flow', () => {
const magicLink = await extractMagicLinkData(page)
if (!magicLink) throw new Error('Failed to extract magic link token and verificationId')

const callbackUrl = `${apiUrl}/reference?token=${magicLink.token}&verificationId=${magicLink.verificationId}`
await page.goto(callbackUrl)
await page.waitForURL(/\/reference$/, { timeout: 15000 })
const callbackUrl = new URL(`${apiUrl}/reference`)
callbackUrl.searchParams.set('token', magicLink.token)
callbackUrl.searchParams.set('verificationId', magicLink.verificationId)
await page.goto(callbackUrl.toString())
await waitForReferenceAuthSettled(page)
await page.waitForLoadState('networkidle')

// Verify logged in state
Expand Down
2 changes: 2 additions & 0 deletions apps/docu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Starts docs site at [http://localhost:3002](http://localhost:3002).
- `pnpm start` - Production server
- `pnpm checktypes` - Type check MDX and TypeScript

Monorepo root (for local API database): `pnpm reset` — see `apps/api/README.md`.

## Content

Documentation content in `content/docs/`:
Expand Down
12 changes: 11 additions & 1 deletion apps/docu/content/docs/adrs/008-database.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,17 @@ All platforms use the same build command pattern:

- **Docker**: `RUN pnpm build` (runs migrations)
- **Railway/Fly.io/Render**: `build: pnpm build` (runs migrations)
- **Local dev**: PostgreSQL migrations run at build time (via `pnpm dev` → `pnpm db:migrate`), PGLite migrations run at runtime
- **Local dev (PostgreSQL)**: Migrations run when you **build** the API (`pnpm build` / `pnpm --filter @repo/api build`), not when you run `pnpm dev` alone. To wipe local Supabase and re-apply schema + seed, use **`pnpm reset`** (repo root) or **`pnpm reset`** in `apps/api`. **PGLite**: migrations still run at runtime

### Local database reset (Supabase + Drizzle)

For local Postgres via Supabase CLI (`project_id: basilic-api` in `apps/api/supabase/config.toml`), use **`pnpm reset`** from the repo root (runs `pnpm --filter @repo/api reset`) or **`pnpm reset`** from `apps/api`. One command does:

1. **`supabase db reset`** — Drops and recreates the database. `[db.seed]` is off; no `seed.sql`.
2. **Drizzle migrations** — `scripts/migrate.ts` via `pnpm db:migrate` with `RUN_PG_MIGRATE=true` and the local Supabase `DATABASE_URL`.
3. **Data seed** — `scripts/seed.ts` (`runSeed`). Add idempotent inserts as needed. Runs only as part of **`pnpm reset`**, not on `pnpm db:migrate` or `pnpm build` alone. See `apps/api/README.md`.

Run `pnpm db:stop` before switching to another project's Supabase instance (e.g. different repo); only one Supabase runs per host.

### Key Benefits

Expand Down
2 changes: 2 additions & 0 deletions apps/docu/content/docs/architecture/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ TypeScript is generated by `@hey-api/openapi-ts`. Other languages can use standa

The API layer uses Drizzle for queries and `drizzle-kit` for migrations. Migration strategy depends on the database runtime; see [ADR 008: Database Platform & Strategy](/docs/adrs/008-database#migration-strategy).

For local Supabase: **`pnpm reset`** from the monorepo root runs Supabase DB reset, Drizzle migrations, and `scripts/seed.ts` (details in ADR 008).

## Security

- **Headers**: X-Content-Type-Options, X-Frame-Options, CSP, HSTS
Expand Down
2 changes: 1 addition & 1 deletion apps/docu/content/docs/development/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,4 @@ flowchart TB

**Optional Setup:**

For database features, you'll need PostgreSQL. See [Installation](/docs/development).
For database features, use local PostgreSQL via Supabase: `pnpm --filter @repo/api db:start`, configure `apps/api/.env`, then apply schema and optional seed with **`pnpm reset`** from the repo root (Supabase reset + Drizzle migrate + `scripts/seed.ts`). See [ADR 008: Database](/docs/adrs/008-database) and `apps/api/README.md` in the monorepo.
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"setup:gitleaks": "node scripts/setup-gitleaks.mjs",
"setup:osv": "node scripts/setup-osv-scanner.mjs",
"setup:database": "node scripts/setup-database.mjs",
"reset": "pnpm --filter @repo/api reset",
"update-deps": "pnpm self-update && pnpm update --latest --recursive"
},
"devDependencies": {
Expand Down Expand Up @@ -61,6 +62,13 @@
],
"overrides": {
"lightningcss": "1.30.1",
"flatted@<3.4.2": "3.4.2",
"h3@<1.15.9": "1.15.9",
"socket.io-parser@4.2.5": "4.2.6",
"undici@5.28.4": "6.24.0",
"undici@6.23.0": "6.24.0",
"undici@7.22.0": "7.24.0",
"next@16.1.6": "16.1.7",
"tar@<7.5.11": "7.5.11",
"basic-ftp@<5.2.0": "5.2.0",
"bn.js@4.12.2": "4.12.3",
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@typescript/native-preview": "7.0.0-dev.20260213.1",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.0.0",
"jsdom": "^28.0.0",
"react": "^19.2.4",
Expand Down
15 changes: 15 additions & 0 deletions packages/react/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { realpathSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vitest/config'

const configDir = dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)

const reactPackageRoot = realpathSync(dirname(require.resolve('react/package.json')))
const reactDomPackageRoot = realpathSync(dirname(require.resolve('react-dom/package.json')))
const reactDomClientEntry = realpathSync(require.resolve('react-dom/client'))

export default defineConfig({
plugins: [react()],
test: {
include: ['src/**/*.{test,spec}.{ts,tsx}'],
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
},
resolve: {
// pnpm: keep symlinked package paths so react-dom and @tanstack/react-query share one react instance
preserveSymlinks: true,
dedupe: ['react', 'react-dom'],
alias: {
'@repo/core': resolve(configDir, '../core/src/index.ts'),
'@repo/ui': resolve(configDir, '../ui/src'),
'@repo/utils': resolve(configDir, '../utils/src'),
react: reactPackageRoot,
'react-dom': reactDomPackageRoot,
'react-dom/client': reactDomClientEntry,
},
},
})
Loading
Loading