Tracked planning docs under docs/ (the project's documentation source of truth per CLAUDE.local.md): - docs/roadmap.md — vision, decisions, architecture, 6-phase roadmap - docs/plans/phase-0-foundation.md — TDD plan to stand up the dockerized Hono + better-auth + Drizzle + libsql backend with an auth round-trip
27 KiB
Phase 0 — Foundation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Stand up a dockerized, dedicated backend service (Hono + better-auth + Drizzle + SQLite/libsql) where a client can sign up, sign in, and use the returned token to call a protected endpoint — proven end-to-end by automated tests and docker compose up.
Architecture: A new greenfield monorepo structure is added alongside the inherited code (which stays untouched as reference). A single backend service (apps/api) owns auth and all DB access; a shared package (packages/shared) holds the TypeScript/zod API contracts. The database is a local SQLite file accessed through libsql (no native compilation — Windows-friendly and identical in Docker).
Tech Stack: Node 22, TypeScript (ESM), Hono, better-auth, Drizzle ORM + drizzle-kit, @libsql/client, zod, Vitest, tsx, Docker.
Global Constraints
- Package manager: Yarn 4 (Berry), already configured at the repo root. Run via corepack.
- Do NOT touch the inherited
apps/mobileorapps/webin this phase — they are reference only. - Package names: scoped
@solelog/*(project is "SoleLog"). This phase adds@solelog/apiand@solelog/shared. - Module system: ESM everywhere (
"type": "module"),moduleResolution: "Bundler"or"NodeNext"in tsconfig. - DB driver: libsql (
@libsql/client) only — neverbetter-sqlite3(avoids native build on Windows). - Dependency majors:
hono@^4,@hono/node-server@^1,better-auth@^1,drizzle-orm@^0.36,drizzle-kit@^0.30,@libsql/client@^0.14,zod@^3,vitest@^3,tsx@^4. Install the latest within each major. - API port:
3000(the inherited web app used 4000 — avoid the clash). - Env vars (apps/api):
DATABASE_URL(e.g.file:./data/app.db),BETTER_AUTH_SECRET,BETTER_AUTH_URL(e.g.http://localhost:3000),PORT. - Commit style: Conventional Commits (
feat:,chore:,test:,docs:), one commit per task step where indicated.
File Structure
apps/api/
package.json
tsconfig.json
drizzle.config.ts
.env.example
src/
index.ts entry — starts the HTTP server
app.ts builds the Hono app (exported for tests)
env.ts reads + validates env vars
db/
client.ts libsql + drizzle client (singleton)
schema.ts better-auth tables (Drizzle)
migrate.ts applies drizzle migrations
auth.ts better-auth config
routes/
health.ts GET /health (public)
me.ts GET /api/me (protected)
drizzle/ generated migration SQL (created by drizzle-kit)
test/
setup.ts vitest global setup: temp DB + migrate
health.test.ts
auth.test.ts
vitest.config.ts
Dockerfile
packages/shared/
package.json
tsconfig.json
src/index.ts zod schemas + types (API contracts)
docker-compose.yml (repo root)
Task 1: Monorepo wiring + shared contracts package
Files:
- Modify:
package.json(root) — addpackages/*toworkspaces - Create:
packages/shared/package.json - Create:
packages/shared/tsconfig.json - Create:
packages/shared/src/index.ts
Interfaces:
-
Produces:
@solelog/sharedexportingHealthResponse(zod schema + type) andMeResponse(zod schema + type), consumed byapps/api. -
Step 1: Add
packages/*to root workspaces
Edit root package.json so the workspaces array is:
"workspaces": [
"apps/*",
"packages/*"
]
- Step 2: Create the shared package manifest
packages/shared/package.json:
{
"name": "@solelog/shared",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"zod": "^3.23.8"
}
}
- Step 3: Create the shared tsconfig
packages/shared/tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true
},
"include": ["src"]
}
- Step 4: Write the shared contracts
packages/shared/src/index.ts:
import { z } from 'zod';
export const HealthResponse = z.object({
status: z.literal('ok'),
});
export type HealthResponse = z.infer<typeof HealthResponse>;
export const PublicUser = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
});
export type PublicUser = z.infer<typeof PublicUser>;
export const MeResponse = z.object({
user: PublicUser,
});
export type MeResponse = z.infer<typeof MeResponse>;
- Step 5: Install and verify the workspace resolves
Run: yarn install
Expected: completes; yarn workspaces list includes @solelog/shared.
Run: yarn workspaces list
Expected output includes a line for packages/shared.
- Step 6: Commit
git add package.json yarn.lock packages/shared
git commit -m "feat(shared): add @solelog/shared contracts package and wire packages/* workspace"
Task 2: Hono backend skeleton + health endpoint
Files:
- Create:
apps/api/package.json - Create:
apps/api/tsconfig.json - Create:
apps/api/src/env.ts - Create:
apps/api/src/app.ts - Create:
apps/api/src/routes/health.ts - Create:
apps/api/src/index.ts - Create:
apps/api/vitest.config.ts - Test:
apps/api/test/health.test.ts
Interfaces:
-
Consumes:
@solelog/shared(HealthResponse). -
Produces:
createApp(): Honofromapps/api/src/app.ts(used by every later task and all tests);envobject fromapps/api/src/env.ts. -
Step 1: Create the api package manifest
apps/api/package.json:
{
"name": "@solelog/api",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/db/migrate.ts"
},
"dependencies": {
"@hono/node-server": "^1.13.7",
"@libsql/client": "^0.14.0",
"@solelog/shared": "workspace:*",
"better-auth": "^1.1.7",
"drizzle-orm": "^0.36.4",
"hono": "^4.6.14",
"zod": "^3.23.8"
},
"devDependencies": {
"drizzle-kit": "^0.30.1",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"vitest": "^3.0.0"
}
}
- Step 2: Install dependencies
Run: yarn install
Expected: resolves and installs the new api dependencies without errors.
- Step 3: Create the api tsconfig
apps/api/tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true,
"types": ["node"],
"noEmit": true
},
"include": ["src", "test"]
}
- Step 4: Create the env reader
apps/api/src/env.ts:
export const env = {
DATABASE_URL: process.env.DATABASE_URL ?? 'file:./data/app.db',
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET ?? 'dev-insecure-secret-change-me',
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL ?? 'http://localhost:3000',
PORT: Number(process.env.PORT ?? 3000),
};
- Step 5: Create the health route
apps/api/src/routes/health.ts:
import { Hono } from 'hono';
import { HealthResponse } from '@solelog/shared';
export const health = new Hono();
health.get('/health', (c) => {
const body: HealthResponse = { status: 'ok' };
return c.json(body);
});
- Step 6: Create the app factory
apps/api/src/app.ts:
import { Hono } from 'hono';
import { health } from './routes/health';
export function createApp(): Hono {
const app = new Hono();
app.route('/', health);
return app;
}
- Step 7: Create the server entry
apps/api/src/index.ts:
import { serve } from '@hono/node-server';
import { createApp } from './app';
import { env } from './env';
const app = createApp();
serve({ fetch: app.fetch, port: env.PORT }, (info) => {
console.log(`API listening on http://localhost:${info.port}`);
});
- Step 8: Create the vitest config
apps/api/vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
setupFiles: ['./test/setup.ts'],
fileParallelism: false,
},
});
- Step 9: Create a no-op setup (real DB setup arrives in Task 3)
apps/api/test/setup.ts:
// Placeholder until Task 3 adds DB migration to the test setup.
export {};
- Step 10: Write the failing health test
apps/api/test/health.test.ts:
import { describe, it, expect } from 'vitest';
import { createApp } from '../src/app';
describe('GET /health', () => {
it('returns { status: "ok" }', async () => {
const app = createApp();
const res = await app.request('/health');
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ status: 'ok' });
});
});
- Step 11: Run the test to verify it passes
Run: yarn workspace @solelog/api test
Expected: PASS (1 test). If createApp or health is missing, fix the files above.
- Step 12: Commit
git add apps/api yarn.lock
git commit -m "feat(api): Hono backend skeleton with /health endpoint and test"
Task 3: Database — Drizzle + libsql + better-auth schema + migrations
Files:
- Create:
apps/api/src/db/schema.ts - Create:
apps/api/src/db/client.ts - Create:
apps/api/drizzle.config.ts - Create:
apps/api/src/db/migrate.ts - Modify:
apps/api/test/setup.ts - Test:
apps/api/test/db.test.ts
Interfaces:
-
Produces:
db(drizzle client) fromapps/api/src/db/client.ts; the better-auth tablesuser,session,account,verificationfromapps/api/src/db/schema.ts;runMigrations()fromapps/api/src/db/migrate.ts(consumed by the test setup and the Docker start). -
Step 1: Define the better-auth Drizzle schema
apps/api/src/db/schema.ts (this is better-auth's standard core schema for SQLite):
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const user = sqliteTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false),
image: text('image'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const session = sqliteTable('session', {
id: text('id').primaryKey(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
token: text('token').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
});
export const account = sqliteTable('account', {
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp' }),
refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }),
scope: text('scope'),
password: text('password'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const verification = sqliteTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }),
updatedAt: integer('updated_at', { mode: 'timestamp' }),
});
Verification (do this during execution): run
npx @better-auth/cli@latest generate --config src/auth.tsafter Task 4 and confirm the generated schema matches the columns above. If better-auth's installed version differs, adopt the generated schema (it is authoritative).
- Step 2: Create the drizzle client
apps/api/src/db/client.ts:
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import { env } from '../env';
import * as schema from './schema';
const client = createClient({ url: env.DATABASE_URL });
export const db = drizzle(client, { schema });
- Step 3: Create the drizzle-kit config
apps/api/drizzle.config.ts:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL ?? 'file:./data/app.db',
},
});
- Step 4: Generate the initial migration
Run: yarn workspace @solelog/api db:generate
Expected: a apps/api/drizzle/0000_*.sql file is created containing CREATE TABLE statements for user, session, account, verification.
- Step 5: Create the migrate runner
apps/api/src/db/migrate.ts:
import { drizzle } from 'drizzle-orm/libsql';
import { migrate } from 'drizzle-orm/libsql/migrator';
import { createClient } from '@libsql/client';
import { env } from '../env';
export async function runMigrations(): Promise<void> {
const client = createClient({ url: env.DATABASE_URL });
const db = drizzle(client);
await migrate(db, { migrationsFolder: './drizzle' });
client.close();
}
// Allow running directly: `tsx src/db/migrate.ts`
if (import.meta.url === `file://${process.argv[1]}`) {
runMigrations()
.then(() => {
console.log('Migrations applied.');
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});
}
- Step 6: Wire migrations into the test setup
Replace apps/api/test/setup.ts with:
import { beforeAll } from 'vitest';
import { rmSync, mkdirSync } from 'node:fs';
// Use a dedicated, freshly-migrated file DB for the test run.
process.env.DATABASE_URL = 'file:./.tmp/test.db';
process.env.BETTER_AUTH_SECRET = 'test-secret';
process.env.BETTER_AUTH_URL = 'http://localhost:3000';
beforeAll(async () => {
rmSync('./.tmp', { recursive: true, force: true });
mkdirSync('./.tmp', { recursive: true });
const { runMigrations } = await import('../src/db/migrate');
await runMigrations();
});
- Step 7: Ignore the test/db scratch dirs
Append to root .gitignore:
# api local data
apps/api/.tmp/
apps/api/data/
- Step 8: Write the DB smoke test
apps/api/test/db.test.ts:
import { describe, it, expect } from 'vitest';
import { db } from '../src/db/client';
import { user } from '../src/db/schema';
describe('database', () => {
it('can query the migrated user table (empty)', async () => {
const rows = await db.select().from(user);
expect(rows).toEqual([]);
});
});
- Step 9: Run tests to verify they pass
Run: yarn workspace @solelog/api test
Expected: PASS (health + db). The migration runs against ./.tmp/test.db in setup; the query returns an empty array.
- Step 10: Commit
git add apps/api .gitignore yarn.lock
git commit -m "feat(api): Drizzle + libsql DB layer with better-auth schema and migrations"
Task 4: better-auth wired into Hono (sign-up / sign-in)
Files:
- Create:
apps/api/src/auth.ts - Modify:
apps/api/src/app.ts - Test:
apps/api/test/auth.test.ts(sign-up + sign-in only; protected route in Task 5)
Interfaces:
-
Consumes:
dbandschema(Task 3). -
Produces:
auth(better-auth instance) fromapps/api/src/auth.ts; mounted handler atALL /api/auth/*. -
Step 1: Create the better-auth config
apps/api/src/auth.ts:
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { bearer } from 'better-auth/plugins';
import { db } from './db/client';
import * as schema from './db/schema';
import { env } from './env';
export const auth = betterAuth({
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
trustedOrigins: [env.BETTER_AUTH_URL, 'http://localhost:3000'],
database: drizzleAdapter(db, { provider: 'sqlite', schema }),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
plugins: [bearer()],
});
- Step 2: Mount the auth handler in the app
Modify apps/api/src/app.ts to:
import { Hono } from 'hono';
import { health } from './routes/health';
import { auth } from './auth';
export function createApp(): Hono {
const app = new Hono();
app.route('/', health);
app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw));
return app;
}
- Step 3: Write the failing sign-up / sign-in test
apps/api/test/auth.test.ts:
import { describe, it, expect } from 'vitest';
import { createApp } from '../src/app';
const json = { 'content-type': 'application/json' };
describe('auth', () => {
it('signs a user up and signs them in, returning a bearer token', async () => {
const app = createApp();
const creds = { email: 'worker@example.com', password: 'sterk-wachtwoord-123', name: 'Worker' };
const signup = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: json,
body: JSON.stringify(creds),
});
expect(signup.status).toBe(200);
const signin = await app.request('/api/auth/sign-in/email', {
method: 'POST',
headers: json,
body: JSON.stringify({ email: creds.email, password: creds.password }),
});
expect(signin.status).toBe(200);
const token = signin.headers.get('set-auth-token');
expect(token).toBeTruthy();
});
});
- Step 4: Run the test to verify it passes
Run: yarn workspace @solelog/api test test/auth.test.ts
Expected: PASS. The set-auth-token response header is the bearer plugin's token.
If the header is absent, confirm the bearer() plugin is registered and check the installed better-auth docs for the token-exposure header name; adjust the assertion to match.
- Step 5: Commit
git add apps/api yarn.lock
git commit -m "feat(api): mount better-auth (email+password + bearer) on /api/auth"
Task 5: Protected route + full auth round-trip (the "done when")
Files:
- Create:
apps/api/src/routes/me.ts - Modify:
apps/api/src/app.ts - Test:
apps/api/test/me.test.ts
Interfaces:
-
Consumes:
auth(Task 4),MeResponsefrom@solelog/shared. -
Produces:
GET /api/me— returns401without a valid token,200with{ user }when the bearer token is valid. -
Step 1: Create the protected route
apps/api/src/routes/me.ts:
import { Hono } from 'hono';
import type { MeResponse } from '@solelog/shared';
import { auth } from '../auth';
export const me = new Hono();
me.get('/api/me', async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
return c.json({ error: 'Unauthorized' }, 401);
}
const body: MeResponse = {
user: {
id: session.user.id,
email: session.user.email,
name: session.user.name,
},
};
return c.json(body);
});
- Step 2: Mount the route
Modify apps/api/src/app.ts to add the me route (full file):
import { Hono } from 'hono';
import { health } from './routes/health';
import { me } from './routes/me';
import { auth } from './auth';
export function createApp(): Hono {
const app = new Hono();
app.route('/', health);
app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw));
app.route('/', me);
return app;
}
- Step 3: Write the failing round-trip test
apps/api/test/me.test.ts:
import { describe, it, expect } from 'vitest';
import { createApp } from '../src/app';
const json = { 'content-type': 'application/json' };
describe('GET /api/me', () => {
it('rejects an unauthenticated request', async () => {
const app = createApp();
const res = await app.request('/api/me');
expect(res.status).toBe(401);
});
it('returns the user for a valid bearer token (sign-up -> sign-in -> me)', async () => {
const app = createApp();
const creds = { email: 'me@example.com', password: 'sterk-wachtwoord-123', name: 'Me' };
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: json,
body: JSON.stringify(creds),
});
const signin = await app.request('/api/auth/sign-in/email', {
method: 'POST',
headers: json,
body: JSON.stringify({ email: creds.email, password: creds.password }),
});
const token = signin.headers.get('set-auth-token');
const res = await app.request('/api/me', {
headers: { authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.user.email).toBe(creds.email);
});
});
- Step 4: Run the test to verify it passes
Run: yarn workspace @solelog/api test
Expected: PASS (health, db, auth, me — all green). This proves the Phase 0 "done when" criterion in code.
- Step 5: Commit
git add apps/api
git commit -m "feat(api): add protected GET /api/me and full auth round-trip test"
Task 6: Dockerize the backend + compose + run docs
Files:
- Create:
apps/api/Dockerfile - Create:
apps/api/.env.example - Create:
docker-compose.yml(repo root) - Create:
apps/api/.dockerignore - Modify:
docs/README.md(run instructions — create if absent)
Interfaces:
-
Produces:
docker compose upserving the API onlocalhost:3000with a persisted SQLite volume and migrations applied at start. -
Step 1: Create the env example
apps/api/.env.example:
DATABASE_URL=file:./data/app.db
BETTER_AUTH_SECRET=change-me-to-a-long-random-string
BETTER_AUTH_URL=http://localhost:3000
PORT=3000
- Step 2: Create the dockerignore
apps/api/.dockerignore:
node_modules
.tmp
data
drizzle/meta
- Step 3: Create the Dockerfile
apps/api/Dockerfile (multi-arch friendly; runs migrations then starts):
FROM node:22-alpine AS base
RUN corepack enable
WORKDIR /repo
# Copy workspace manifests for cached install
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn/ ./.yarn/
COPY packages/shared/package.json ./packages/shared/package.json
COPY apps/api/package.json ./apps/api/package.json
# Copy the other workspace manifests too, so the workspace graph matches the
# lockfile (otherwise Yarn errors on a mismatch). `focus` still installs ONLY
# the @solelog/api subtree, so mobile/web deps are not pulled.
COPY apps/mobile/package.json ./apps/mobile/package.json
COPY apps/web/package.json ./apps/web/package.json
RUN yarn workspaces focus @solelog/api
# Copy sources
COPY packages/shared/ ./packages/shared/
COPY apps/api/ ./apps/api/
WORKDIR /repo/apps/api
ENV NODE_ENV=production
EXPOSE 3000
# Apply migrations, then start the server
CMD ["sh", "-c", "yarn db:migrate && yarn start"]
Note:
yarn workspaces focusrequires theplugin-workspace-tools(bundled with Yarn 4). If it is unavailable, substituteRUN yarn install --immutable.
- Step 4: Create the compose file
docker-compose.yml (repo root):
services:
api:
build:
context: .
dockerfile: apps/api/Dockerfile
ports:
- '3000:3000'
environment:
DATABASE_URL: file:/data/app.db
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-change-me-to-a-long-random-string}
BETTER_AUTH_URL: http://localhost:3000
PORT: '3000'
volumes:
- solelog_db:/data
volumes:
solelog_db:
- Step 5: Build and start the stack
Run: docker compose up --build -d
Expected: the api service builds and starts; docker compose logs api shows "Migrations applied." then "API listening on http://localhost:3000".
- Step 6: Smoke-test the running container
Run: curl -s http://localhost:3000/health
Expected: {"status":"ok"}
Run (full round-trip against the container):
curl -s -X POST http://localhost:3000/api/auth/sign-up/email \
-H 'content-type: application/json' \
-d '{"email":"smoke@example.com","password":"sterk-wachtwoord-123","name":"Smoke"}'
TOKEN=$(curl -s -D - -o /dev/null -X POST http://localhost:3000/api/auth/sign-in/email \
-H 'content-type: application/json' \
-d '{"email":"smoke@example.com","password":"sterk-wachtwoord-123"}' \
| grep -i '^set-auth-token:' | awk '{print $2}' | tr -d '\r')
curl -s http://localhost:3000/api/me -H "authorization: Bearer $TOKEN"
Expected: the final call returns {"user":{...,"email":"smoke@example.com"}}, confirming the token authorises /api/me.
- Step 7: Tear down
Run: docker compose down
Expected: container stops; the solelog_db volume persists for next time.
- Step 8: Write run instructions
Create docs/README.md:
# SoleLog docs
- [Roadmap & project overview](./roadmap.md)
- [Plans](./plans/)
## Running the backend (Phase 0)
Local (dev):
\`\`\`bash
yarn install
cp apps/api/.env.example apps/api/.env # edit BETTER_AUTH_SECRET
yarn workspace @solelog/api db:migrate
yarn workspace @solelog/api dev
\`\`\`
Docker (whole stack):
\`\`\`bash
docker compose up --build
# health: curl http://localhost:3000/health
\`\`\`
- Step 9: Commit
git add apps/api/Dockerfile apps/api/.env.example apps/api/.dockerignore docker-compose.yml docs/README.md
git commit -m "feat(api): dockerize backend with compose, migrations-on-start, and run docs"
Definition of Done (Phase 0)
yarn workspace @solelog/api testis green (health, db, auth, me).docker compose up --buildserves/healthand the sign-up → sign-in →/api/meround-trip works against the container.- The inherited
apps/mobileandapps/webare untouched. - All work committed;
docs/roadmap.mdand this plan are tracked.
Notes for the executor (version-drift safety)
better-auth, Drizzle, and drizzle-kit evolve quickly. Three checkpoints where the installed version is authoritative over this plan's sample code:
- Schema (Task 3): confirm with
npx @better-auth/cli@latest generate. - Bearer token header (Tasks 4–5): the assertion reads
set-auth-token; verify against the installed bearer-plugin docs. - drizzle-kit config (Task 3): if
dialect: 'sqlite'rejects the libsqlfile:URL, switch todialect: 'turso'withdbCredentials.url. The TDD loop (write test → run → fix) will surface any mismatch immediately.