diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..4664b0644 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +NODE_ENV=development +WEB_PORT=3000 +GATEWAY_PORT=4000 +IDENTITY_SERVICE_PORT=4101 +PLANNING_SERVICE_PORT=4102 +HABIT_SERVICE_PORT=4103 +REVIEW_SERVICE_PORT=4104 +DATABASE_URL=postgresql://lifeos:lifeos@postgres:5432/lifeos +NATS_URL=nats://nats:4222 +CORS_ALLOWED_ORIGINS=http://localhost:3000 +SESSION_SECRET=replace-with-at-least-32-random-bytes +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +OAUTH_CALLBACK_BASE_URL=http://localhost:4000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..efe000d8e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Check formatting + run: pnpm format:check + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build + + - name: Validate Compose + run: docker compose config --quiet diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..dd72e9dea --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +.pnpm-store/ +.turbo/ +.next/ +dist/ +coverage/ +.env +.env.* +!.env.example +*.log +.DS_Store +.idea/ +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 000000000..037cae6c1 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# LifeOS + +**The open-source personal operating system for goals, projects, tasks, habits, and reviews.** + +LifeOS connects everyday action to longer-term direction. It is designed as a multi-user, self-hostable SaaS with domain-oriented microservices, user-owned data, and auditable AI assistance. + +## Status + +LifeOS is in active foundation development. The current `develop` branch contains the initial monorepo, gateway, service skeletons, shared contracts, responsive web shell, PostgreSQL, and NATS JetStream configuration. + +## Architecture + +```text +Web / PWA + | +API Gateway / BFF + |---------------------------------------------| +Identity Planning Habit Review + | | | | +PostgreSQL schemas / databases + NATS JetStream events +``` + +The MVP deliberately keeps goals, projects, milestones, and tasks in one Planning bounded context. Services own their persistence boundaries; direct cross-service table access is prohibited. + +## Repository layout + +```text +apps/ + web/ + gateway/ + identity-service/ + planning-service/ + habit-service/ + review-service/ +packages/ + contracts/ +infra/ +docs/ +``` + +## Prerequisites + +- Node.js 22+ +- pnpm 10+ +- Docker with Compose + +## Local development + +```bash +cp .env.example .env +corepack enable +pnpm install +docker compose up -d +pnpm dev +``` + +Default endpoints: + +- Web: `http://localhost:3000` +- Gateway health: `http://localhost:4000/v1/health` +- Gateway Today composition: `http://localhost:4000/v1/today` +- NATS monitoring: `http://localhost:8222` + +## Authentication + +Google and GitHub OAuth are the required login providers. Provider credentials are supplied through environment variables and must never be committed. OAuth flows are part of the next implementation slice; the current web actions are interface placeholders. + +## Privacy + +This is a public repository. It contains synthetic examples only. Personal goals, health information, relationship data, credentials, access tokens, and production exports must not be committed. + +## Documentation + +- Product and architecture design: `docs/superpowers/specs/2026-08-02-life-os-design.md` +- Foundation implementation plan: `docs/superpowers/plans/2026-08-02-life-os-foundation.md` + +## Contributing + +Development work targets `develop` and reaches `main` through reviewed pull requests. Keep service boundaries explicit, update contracts before consumers, add tests with behavior changes, and avoid introducing infrastructure that has no measured need. + +## License + +A project license will be selected before the first public release. Until then, the source is publicly visible but no license grant is implied. diff --git a/apps/gateway/package.json b/apps/gateway/package.json new file mode 100644 index 000000000..7ea8cfc41 --- /dev/null +++ b/apps/gateway/package.json @@ -0,0 +1,26 @@ +{ + "name": "@life-os/gateway", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "lint": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@life-os/contracts": "workspace:*", + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/platform-express": "^11.1.6", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.10", + "@types/node": "^24.3.0", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + } +} diff --git a/apps/gateway/src/app.module.ts b/apps/gateway/src/app.module.ts new file mode 100644 index 000000000..878026f22 --- /dev/null +++ b/apps/gateway/src/app.module.ts @@ -0,0 +1,23 @@ +import { Controller, Get, Module } from '@nestjs/common'; + +@Controller() +class HealthController { + @Get('health') + health(): { status: 'ok'; service: 'gateway' } { + return { status: 'ok', service: 'gateway' }; + } + + @Get('today') + today(): { tasks: unknown[]; habits: unknown[]; message: string } { + return { + tasks: [], + habits: [], + message: 'Today composition endpoint is ready for domain-service integration.', + }; + } +} + +@Module({ + controllers: [HealthController], +}) +export class AppModule {} diff --git a/apps/gateway/src/main.ts b/apps/gateway/src/main.ts new file mode 100644 index 000000000..41cb4f597 --- /dev/null +++ b/apps/gateway/src/main.ts @@ -0,0 +1,24 @@ +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +function getAllowedOrigins(): string[] { + return (process.env.CORS_ALLOWED_ORIGINS ?? 'http://localhost:3000') + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean); +} + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + app.enableCors({ + origin: getAllowedOrigins(), + credentials: true, + }); + app.setGlobalPrefix('v1'); + app.enableShutdownHooks(); + const port = Number(process.env.GATEWAY_PORT ?? 4000); + await app.listen(port, '0.0.0.0'); +} + +void bootstrap(); diff --git a/apps/gateway/tsconfig.json b/apps/gateway/tsconfig.json new file mode 100644 index 000000000..3bdcd8173 --- /dev/null +++ b/apps/gateway/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "rootDir": "src", + "outDir": "dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false + }, + "include": ["src/**/*.ts"] +} diff --git a/apps/habit-service/package.json b/apps/habit-service/package.json new file mode 100644 index 000000000..4857ed3ee --- /dev/null +++ b/apps/habit-service/package.json @@ -0,0 +1,25 @@ +{ + "name": "@life-os/habit-service", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "lint": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/platform-express": "^11.1.6", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.10", + "@types/node": "^24.3.0", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + } +} diff --git a/apps/habit-service/src/main.ts b/apps/habit-service/src/main.ts new file mode 100644 index 000000000..d0646e00d --- /dev/null +++ b/apps/habit-service/src/main.ts @@ -0,0 +1,22 @@ +import 'reflect-metadata'; +import { Controller, Get, Module } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; + +@Controller() +class HealthController { + @Get('health') + health(): { status: 'ok'; service: 'habit-service' } { + return { status: 'ok', service: 'habit-service' }; + } +} + +@Module({ controllers: [HealthController] }) +class AppModule {} + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + app.enableShutdownHooks(); + await app.listen(Number(process.env.HABIT_SERVICE_PORT ?? 4103), '0.0.0.0'); +} + +void bootstrap(); diff --git a/apps/habit-service/tsconfig.json b/apps/habit-service/tsconfig.json new file mode 100644 index 000000000..d0a5f10ad --- /dev/null +++ b/apps/habit-service/tsconfig.json @@ -0,0 +1 @@ +{"extends":"../../tsconfig.base.json","compilerOptions":{"module":"CommonJS","moduleResolution":"Node","rootDir":"src","outDir":"dist","experimentalDecorators":true,"emitDecoratorMetadata":true,"declaration":false},"include":["src/**/*.ts"]} diff --git a/apps/identity-service/package.json b/apps/identity-service/package.json new file mode 100644 index 000000000..95a4dba28 --- /dev/null +++ b/apps/identity-service/package.json @@ -0,0 +1,25 @@ +{ + "name": "@life-os/identity-service", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "lint": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/platform-express": "^11.1.6", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.10", + "@types/node": "^24.3.0", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + } +} diff --git a/apps/identity-service/src/main.ts b/apps/identity-service/src/main.ts new file mode 100644 index 000000000..d6b415520 --- /dev/null +++ b/apps/identity-service/src/main.ts @@ -0,0 +1,22 @@ +import 'reflect-metadata'; +import { Controller, Get, Module } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; + +@Controller() +class HealthController { + @Get('health') + health(): { status: 'ok'; service: 'identity-service' } { + return { status: 'ok', service: 'identity-service' }; + } +} + +@Module({ controllers: [HealthController] }) +class AppModule {} + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + app.enableShutdownHooks(); + await app.listen(Number(process.env.IDENTITY_SERVICE_PORT ?? 4101), '0.0.0.0'); +} + +void bootstrap(); diff --git a/apps/identity-service/tsconfig.json b/apps/identity-service/tsconfig.json new file mode 100644 index 000000000..d0a5f10ad --- /dev/null +++ b/apps/identity-service/tsconfig.json @@ -0,0 +1 @@ +{"extends":"../../tsconfig.base.json","compilerOptions":{"module":"CommonJS","moduleResolution":"Node","rootDir":"src","outDir":"dist","experimentalDecorators":true,"emitDecoratorMetadata":true,"declaration":false},"include":["src/**/*.ts"]} diff --git a/apps/planning-service/package.json b/apps/planning-service/package.json new file mode 100644 index 000000000..b4110cbab --- /dev/null +++ b/apps/planning-service/package.json @@ -0,0 +1,25 @@ +{ + "name": "@life-os/planning-service", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "lint": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/platform-express": "^11.1.6", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.10", + "@types/node": "^24.3.0", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + } +} diff --git a/apps/planning-service/src/main.ts b/apps/planning-service/src/main.ts new file mode 100644 index 000000000..f873a59f3 --- /dev/null +++ b/apps/planning-service/src/main.ts @@ -0,0 +1,22 @@ +import 'reflect-metadata'; +import { Controller, Get, Module } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; + +@Controller() +class HealthController { + @Get('health') + health(): { status: 'ok'; service: 'planning-service' } { + return { status: 'ok', service: 'planning-service' }; + } +} + +@Module({ controllers: [HealthController] }) +class AppModule {} + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + app.enableShutdownHooks(); + await app.listen(Number(process.env.PLANNING_SERVICE_PORT ?? 4102), '0.0.0.0'); +} + +void bootstrap(); diff --git a/apps/planning-service/tsconfig.json b/apps/planning-service/tsconfig.json new file mode 100644 index 000000000..d0a5f10ad --- /dev/null +++ b/apps/planning-service/tsconfig.json @@ -0,0 +1 @@ +{"extends":"../../tsconfig.base.json","compilerOptions":{"module":"CommonJS","moduleResolution":"Node","rootDir":"src","outDir":"dist","experimentalDecorators":true,"emitDecoratorMetadata":true,"declaration":false},"include":["src/**/*.ts"]} diff --git a/apps/review-service/package.json b/apps/review-service/package.json new file mode 100644 index 000000000..113d64f21 --- /dev/null +++ b/apps/review-service/package.json @@ -0,0 +1,25 @@ +{ + "name": "@life-os/review-service", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "lint": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/platform-express": "^11.1.6", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.10", + "@types/node": "^24.3.0", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + } +} diff --git a/apps/review-service/src/main.ts b/apps/review-service/src/main.ts new file mode 100644 index 000000000..fe2f85f57 --- /dev/null +++ b/apps/review-service/src/main.ts @@ -0,0 +1,22 @@ +import 'reflect-metadata'; +import { Controller, Get, Module } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; + +@Controller() +class HealthController { + @Get('health') + health(): { status: 'ok'; service: 'review-service' } { + return { status: 'ok', service: 'review-service' }; + } +} + +@Module({ controllers: [HealthController] }) +class AppModule {} + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + app.enableShutdownHooks(); + await app.listen(Number(process.env.REVIEW_SERVICE_PORT ?? 4104), '0.0.0.0'); +} + +void bootstrap(); diff --git a/apps/review-service/tsconfig.json b/apps/review-service/tsconfig.json new file mode 100644 index 000000000..d0a5f10ad --- /dev/null +++ b/apps/review-service/tsconfig.json @@ -0,0 +1 @@ +{"extends":"../../tsconfig.base.json","compilerOptions":{"module":"CommonJS","moduleResolution":"Node","rootDir":"src","outDir":"dist","experimentalDecorators":true,"emitDecoratorMetadata":true,"declaration":false},"include":["src/**/*.ts"]} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 000000000..e2eb016ae --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next'; +import './styles.css'; + +export const metadata: Metadata = { + title: 'LifeOS', + description: 'Open-source goals, projects, tasks, habits, and reviews.', +}; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx new file mode 100644 index 000000000..94da150c3 --- /dev/null +++ b/apps/web/app/page.tsx @@ -0,0 +1,55 @@ +const navItems = ['Today', 'Goals', 'Projects', 'Tasks', 'Habits', 'Review']; + +const cards = [ + { title: 'Daily priorities', value: '0 / 3', detail: 'Choose the three outcomes that matter today.' }, + { title: 'Active goals', value: '0', detail: 'Connect everyday work to a meaningful direction.' }, + { title: 'Habit adherence', value: '—', detail: 'History will appear after the first completion.' }, +]; + +export default function HomePage() { + return ( +
+ + +
+
+

Sunday · Personal workspace

+

Make today serve something larger.

+

Capture what matters, connect it to a goal, and choose the next concrete action.

+
+ +
+ {cards.map((card) => ( +
+

{card.title}

+ {card.value} + {card.detail} +
+ ))} +
+ +
+
+

Today

+

No tasks yet

+

Add a task, project, goal, or recurring habit. LifeOS will keep the relationship between them visible.

+
+ +
+
+
+ ); +} diff --git a/apps/web/app/styles.css b/apps/web/app/styles.css new file mode 100644 index 000000000..df7f3a3b1 --- /dev/null +++ b/apps/web/app/styles.css @@ -0,0 +1,77 @@ +:root { + color-scheme: light; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #f4f7f2; + color: #172018; +} + +* { box-sizing: border-box; } +body { margin: 0; min-height: 100vh; } +button, a { font: inherit; } +button { cursor: pointer; } + +.app-shell { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 32px; + padding: 28px 22px; + background: #102d21; + color: #f4fbf6; +} + +.brand { font-size: 1.35rem; font-weight: 800; letter-spacing: -0.03em; } +nav { display: grid; gap: 6px; } +nav a { + color: #c9ddd0; + text-decoration: none; + padding: 11px 12px; + border-radius: 10px; +} +nav a:hover, nav a:focus-visible, nav a.active { background: #214b38; color: white; outline: none; } + +.auth-actions { margin-top: auto; display: grid; gap: 10px; } +.auth-actions button, .primary { + border: 0; + border-radius: 10px; + padding: 11px 14px; + background: #f3cd68; + color: #172018; + font-weight: 700; +} +.auth-actions .secondary { background: transparent; color: white; border: 1px solid #5c7a68; } + +.content { padding: 64px clamp(24px, 6vw, 92px); } +header { max-width: 780px; } +.eyebrow { margin: 0 0 10px; color: #587061; font-size: .78rem; font-weight: 800; text-transform: uppercase; letter-spacing: .12em; } +h1 { margin: 0; font-size: clamp(2.4rem, 6vw, 5.5rem); line-height: .98; letter-spacing: -.06em; } +.lede { max-width: 650px; font-size: 1.08rem; color: #526157; line-height: 1.7; } + +.metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; margin: 48px 0 24px; } +.metric-card, .today-panel { background: white; border: 1px solid #dfe7df; border-radius: 18px; box-shadow: 0 12px 30px rgba(31, 54, 39, .06); } +.metric-card { padding: 22px; display: grid; gap: 10px; } +.metric-card p { margin: 0; color: #607066; font-weight: 700; } +.metric-card strong { font-size: 2rem; } +.metric-card span { color: #69766e; line-height: 1.45; } +.today-panel { padding: 28px; display: flex; align-items: center; justify-content: space-between; gap: 24px; } +.today-panel h2 { margin: 0 0 8px; font-size: 1.6rem; } +.today-panel p:last-child { margin: 0; color: #66746b; max-width: 680px; line-height: 1.55; } + +@media (max-width: 900px) { + .app-shell { grid-template-columns: 1fr; } + .sidebar { position: static; padding: 18px; } + nav { grid-template-columns: repeat(3, 1fr); } + .auth-actions { display: none; } + .metrics { grid-template-columns: 1fr; } +} + +@media (max-width: 560px) { + nav { grid-template-columns: repeat(2, 1fr); } + .content { padding: 36px 18px; } + .today-panel { align-items: flex-start; flex-direction: column; } +} diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts new file mode 100644 index 000000000..791ad6643 --- /dev/null +++ b/apps/web/next-env.d.ts @@ -0,0 +1,4 @@ +/// +/// + +// This file is generated-compatible and should not be edited manually. diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 000000000..6ec3d12ff --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,23 @@ +{ + "name": "@life-os/web", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build", + "dev": "next dev -p 3000", + "lint": "tsc --noEmit", + "test": "node --test", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.5.2", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "typescript": "^5.9.2" + } +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 000000000..1770f9fda --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "preserve", + "noEmit": true, + "incremental": true, + "plugins": [{ "name": "next" }] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..a89ee78f5 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,34 @@ +services: + postgres: + image: postgres:17-alpine + environment: + POSTGRES_USER: lifeos + POSTGRES_PASSWORD: lifeos + POSTGRES_DB: lifeos + ports: + - "5432:5432" + volumes: + - lifeos-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U lifeos -d lifeos"] + interval: 5s + timeout: 5s + retries: 10 + + nats: + image: nats:2.11-alpine + command: ["-js", "-m", "8222"] + ports: + - "4222:4222" + - "8222:8222" + volumes: + - lifeos-nats:/data + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8222/healthz"] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + lifeos-postgres: + lifeos-nats: diff --git a/docs/superpowers/plans/2026-08-02-life-os-foundation.md b/docs/superpowers/plans/2026-08-02-life-os-foundation.md new file mode 100644 index 000000000..844b85c62 --- /dev/null +++ b/docs/superpowers/plans/2026-08-02-life-os-foundation.md @@ -0,0 +1,85 @@ +# LifeOS 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:** Deliver a runnable MSA foundation with a Next.js web app, NestJS gateway and domain services, shared contracts, PostgreSQL, NATS JetStream, Docker Compose, and CI. + +**Architecture:** A pnpm/Turborepo monorepo contains independently deployable services. The gateway is the only public API entry point. Each domain service owns its schema boundary and communicates asynchronously through versioned NATS events. + +**Tech Stack:** TypeScript, pnpm, Turborepo, Next.js, NestJS, PostgreSQL, NATS JetStream, Vitest, Docker Compose, GitHub Actions. + +## Global Constraints + +- Public repository; no personal todo data or secrets. +- Google and GitHub OAuth are the required login providers. +- Service boundaries follow domains, not individual entities. +- External synchronous contracts use REST/OpenAPI. +- Asynchronous contracts use versioned JSON Schema. +- All persisted timestamps are UTC; user timezone is an IANA timezone. +- AI features remain advisory and cannot silently mutate user data. + +--- + +### Task 1: Monorepo foundation + +**Files:** root package/config files, shared TypeScript config, README, environment example. + +- [ ] Create pnpm workspace and Turborepo configuration. +- [ ] Add root scripts for build, test, lint, typecheck, and development. +- [ ] Document local prerequisites and repository layout. +- [ ] Commit the runnable workspace foundation. + +### Task 2: Shared contracts + +**Files:** `packages/contracts`, `packages/event-schemas`, `packages/config`. + +- [ ] Define problem-details API errors and identity/workspace request context. +- [ ] Define versioned domain-event envelope and task-completed schema. +- [ ] Add unit tests for contract validation. +- [ ] Commit shared contracts. + +### Task 3: Gateway and service skeletons + +**Files:** `apps/gateway`, `apps/identity-service`, `apps/planning-service`, `apps/habit-service`, `apps/review-service`. + +- [ ] Add health/readiness endpoints to every service. +- [ ] Add gateway `/v1/health` and `/v1/today` composition placeholder. +- [ ] Add structured configuration loading. +- [ ] Add smoke tests for each application. +- [ ] Commit service skeletons. + +### Task 4: Web application foundation + +**Files:** `apps/web`. + +- [ ] Create responsive Next.js shell. +- [ ] Add Today, Goals, Projects, Tasks, Habits, and Review navigation. +- [ ] Add Google and GitHub login entry points as disabled configuration-aware actions. +- [ ] Add accessibility smoke test and production build. +- [ ] Commit the web foundation. + +### Task 5: Local infrastructure + +**Files:** `infra/compose`, root `compose.yaml`, Dockerfiles. + +- [ ] Add PostgreSQL and NATS JetStream. +- [ ] Add application containers and health checks. +- [ ] Add `.env.example` with non-secret placeholders. +- [ ] Verify `docker compose config` succeeds. +- [ ] Commit local infrastructure. + +### Task 6: CI and repository quality + +**Files:** `.github/workflows/ci.yml`, lint/format configs. + +- [ ] Run install with frozen lockfile. +- [ ] Run formatting, lint, typecheck, tests, and builds. +- [ ] Validate Docker Compose configuration. +- [ ] Add dependency review and secret-safety guidance. +- [ ] Commit CI. + +### Task 7: Review and pull request + +- [ ] Verify repository files contain no personal data or credentials. +- [ ] Review the develop-to-main diff. +- [ ] Open a draft pull request describing architecture, validation, and follow-up work. diff --git a/package.json b/package.json new file mode 100644 index 000000000..b4548fb17 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@life-os/root", + "version": "0.1.0", + "private": true, + "packageManager": "pnpm@10.15.0", + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev --parallel", + "lint": "turbo run lint", + "test": "turbo run test", + "typecheck": "turbo run typecheck", + "format:check": "prettier --check README.md package.json turbo.json tsconfig.base.json pnpm-workspace.yaml compose.yaml .github/workflows/ci.yml", + "format": "prettier --write ." + }, + "devDependencies": { + "prettier": "^3.6.2", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + } +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 000000000..f206a9ec0 --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,14 @@ +{ + "name": "@life-os/contracts", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "tsc -p tsconfig.json --noEmit", + "test": "node --test" + } +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts new file mode 100644 index 000000000..e1a97e3f8 --- /dev/null +++ b/packages/contracts/src/index.ts @@ -0,0 +1,42 @@ +export type WorkspaceRole = 'owner' | 'admin' | 'member' | 'viewer'; + +export interface RequestContext { + userId: string; + workspaceId: string; + role: WorkspaceRole; + correlationId: string; +} + +export interface ProblemDetails { + type: string; + title: string; + status: number; + detail?: string; + instance?: string; + code: string; + correlationId: string; + errors?: Record; +} + +export interface DomainEvent { + id: string; + type: string; + version: 1; + occurredAt: string; + actorId: string; + workspaceId: string; + correlationId: string; + causationId?: string; + payload: TPayload; +} + +export interface TaskCompletedPayload { + taskId: string; + projectId?: string; + goalId?: string; + completedAt: string; +} + +export type TaskCompletedEvent = DomainEvent & { + type: 'planning.task.completed.v1'; +}; diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 000000000..df59da578 --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000..286cf7f56 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - apps/* + - packages/* diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..87f46cdeb --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 000000000..84a603999 --- /dev/null +++ b/turbo.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": { + "dependsOn": ["^lint"] + }, + "test": { + "dependsOn": ["^build"], + "outputs": ["coverage/**"] + }, + "typecheck": { + "dependsOn": ["^typecheck"] + } + } +}