{"id":50767597,"url":"https://github.com/roymcfarland/workflow-blueprint","last_synced_at":"2026-06-11T15:02:08.333Z","repository":{"id":359605913,"uuid":"1219577193","full_name":"roymcfarland/workflow-blueprint","owner":"roymcfarland","description":"Workflow Blueprint is an invite-gated Next.js App Router task planning workspace with admin-issued invitations, board-based task management, notes, profile settings, Resend-backed transactional email, and a key-authenticated external API (/api/external/v1/*) that other projects under the owner's control consume.","archived":false,"fork":false,"pushed_at":"2026-06-06T19:24:01.000Z","size":678,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-06T21:13:50.955Z","etag":null,"topics":["ai-coding","case-study","codex","cursor","developer-experience","multi-agent","nextjs","openapi","prisma","prs"],"latest_commit_sha":null,"homepage":"https://workflowblueprint.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/roymcfarland.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-24T02:36:20.000Z","updated_at":"2026-06-06T19:24:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/roymcfarland/workflow-blueprint","commit_stats":null,"previous_names":["roymcfarland/workflow-blueprint"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/roymcfarland/workflow-blueprint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roymcfarland%2Fworkflow-blueprint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roymcfarland%2Fworkflow-blueprint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roymcfarland%2Fworkflow-blueprint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roymcfarland%2Fworkflow-blueprint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roymcfarland","download_url":"https://codeload.github.com/roymcfarland/workflow-blueprint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roymcfarland%2Fworkflow-blueprint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34204197,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-coding","case-study","codex","cursor","developer-experience","multi-agent","nextjs","openapi","prisma","prs"],"created_at":"2026-06-11T15:02:03.940Z","updated_at":"2026-06-11T15:02:08.326Z","avatar_url":"https://github.com/roymcfarland.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Workflow Blueprint\n\nWorkflow Blueprint is an invite-gated Next.js App Router task planning workspace with admin-issued invitations, board-based task management, notes, profile settings, Resend-backed transactional email, and a key-authenticated external API (`/api/external/v1/*`) that other projects under the owner's control consume.\n\nThe live deployment is at [https://www.workflowblueprint.io](https://www.workflowblueprint.io).\n\n## What this repo demonstrates\n\nThis repository is published as a **showcase of a multi-agent development workflow with PR-level guardrails**, not just as a working product. The artifacts of that workflow are checked in alongside the code:\n\n- **Strategic source of truth.** [`PROJECT.md`](./PROJECT.md) defines the product's purpose, non-goals, and the resolved open questions (Q1–Q6) that act as durable Verifier rules. Any PR that violates these rules is an automatic reject.\n- **Tactical agent runbook.** [`AGENTS.md`](./AGENTS.md) is the operational quickstart that Builder agents (OpenAI Codex) read before writing code, plus dev-environment gotchas.\n- **Walked-through case study.** [`CASE_STUDY.md`](./CASE_STUDY.md) traces a single PR (`#13`) end-to-end: the Builder prompt, the diff Codex returned, the Verifier rule it triggered (the Q6 scope-discipline rule), and how the rule itself was born from that PR.\n- **Machine-readable API contract with CI drift detection.** [`docs/openapi.yaml`](./docs/openapi.yaml) is generated from Zod schemas in [`src/lib/external-contract.ts`](./src/lib/external-contract.ts); a CI test (`tests/api/external/openapi.test.ts`) fails any PR where the committed spec diverges from the schemas.\n- **Real CI gates, not vibes.** [`.github/workflows/ci.yml`](./.github/workflows/ci.yml) runs three parallel jobs (`lint`, `test`, `smoke`) on every PR, with a Postgres service container backing the integration and smoke suites.\n\nThe most informative entry points are [`PROJECT.md`](./PROJECT.md), the merged PR history (especially `#7`, `#10`, `#13`, and `#14`), and [`CASE_STUDY.md`](./CASE_STUDY.md).\n\n## What I would do differently\n\nA short, honest retrospective. Three things I would change if I were starting this repo over today:\n\n1. **Write `PROJECT.md` on day one, not at PR 5.** The Builder/Verifier handoff was installed in `#5` after several feature PRs had already merged. Several of those earlier PRs would have been smaller and more focused if the scope-discipline rule (Q6) had existed when they were prompted. Lesson: the strategic document should be the *first* commit, even when its contents are still rough.\n2. **Adopt path-versioned external APIs from the first endpoint.** The original API lived at `/api/external/daily-summary` and `/api/read-only/*`. Migrating to `/api/external/v1/*` required PR 3 (consumer migration in another repo) and PR 4 (legacy alias removal) before the contract could be cleanly versioned. Starting with `/v1/` from day one would have eliminated both PRs.\n3. **Treat the Builder prompt as a reviewable artifact.** Q6 (\"out-of-scope changes must be declared in the PR body\") only became enforceable after `#13` shipped a correct-but-unauthorized SQL rewrite. If the Builder prompt itself were checked into the PR description from the start, scope drift would be auditable from day one rather than caught reactively.\n\n## Stack\n\n- Next.js 16 App Router and React 19\n- Prisma 6 with PostgreSQL persistence (currently hosted on Supabase)\n- Tailwind CSS 4 with custom blueprint design tokens\n- Zod validation on all API payloads\n- Signed HTTP-only session cookies with `jose`\n- Resend transactional email for welcome and password reset messages\n\n## Getting Started\n\n```bash\nnpm install\nnpm run db:deploy\nnpm run db:seed\nnpm run dev\n```\n\nThe dev server starts Next.js on `127.0.0.1`. Run `npm run db:deploy` before the first deploy, and run `npm run db:seed` only when you want the demo account and starter boards in the configured database.\n\nThe seed command reads the demo account password from the required `DEMO_USER_PASSWORD` environment variable and refuses to run when `NODE_ENV=production` or `VERCEL_ENV=production` unless `ALLOW_PRODUCTION_SEED=true` is also set. Choose a unique value per environment and rotate it.\n\n```text\nDEMO_USER_PASSWORD=\"choose-a-strong-password-of-12-or-more-chars\"\nnpm run db:seed\n```\n\n## Environment\n\nCreate `.env.local` for local work:\n\n```bash\nDATABASE_URL=\"postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres?sslmode=require\"\nAUTH_SECRET=\"replace-with-a-long-random-secret\"\nNEXT_PUBLIC_SITE_URL=\"https://www.workflowblueprint.io\"\nRESEND_API_KEY=\"re_...\"\nEMAIL_FROM=\"Workflow Blueprint \u003chello@workflowblueprint.io\u003e\"\nEXTERNAL_API_KEY=\"replace-with-the-shared-external-api-key\"\nEXTERNAL_USER_ID=\"user_demo_alex_blue\"\n```\n\nWhen the project is linked in Vercel, you can pull local secrets without printing them:\n\n```bash\nnpx vercel@latest env pull .env.local --environment=development\n```\n\n`DATABASE_URL` must be a PostgreSQL 14+ connection string. Supabase Postgres is the recommended example and current production host, but any compatible durable PostgreSQL database works. If the Vercel/Supabase integration provides `POSTGRES_PRISMA_URL`, `POSTGRES_URL`, or `POSTGRES_URL_NON_POOLING` instead, the app will use those automatically.\nPrisma CLI commands prefer `POSTGRES_URL_NON_POOLING` when it is available.\nUse a durable PostgreSQL 14+ database for production account creation.\n`AUTH_SECRET` must be a long random secret in production.\n`NEXT_PUBLIC_SITE_URL` is used to generate absolute canonical and social sharing metadata.\n`RESEND_API_KEY` and `EMAIL_FROM` enable welcome emails and production password reset emails. Local development can omit them; reset requests will expose a preview link instead.\n`EXTERNAL_API_KEY` enables the external `/api/external/v1/*` API. `EXTERNAL_USER_ID` selects which account the external API surfaces; when unset it falls back to the seeded demo user.\n\nOptional server-side Sentry settings:\n\n| Variable | Required | Description |\n| --- | --- | --- |\n| `SENTRY_DSN` | No | Enables Sentry server-side error capture when set. Leave unset for local dev. |\n| `SENTRY_ENVIRONMENT` | No | Override for the Sentry environment tag. Defaults to `VERCEL_ENV` or `\"development\"`. |\n| `SENTRY_RELEASE` | No | Override for the Sentry release tag. Defaults to `VERCEL_GIT_COMMIT_SHA`. |\n\n## Database Setup\n\nApply the checked-in Prisma migrations to the database before enabling signup:\n\n```bash\nnpm run db:deploy\n```\n\nFor a brand-new database, optionally seed the demo account:\n\n```bash\nnpm run db:seed\n```\n\nIf the database runtime URL uses a pooler and migration deployment fails, temporarily run `npm run db:deploy` with the direct connection string in `DATABASE_URL`, then keep the Vercel runtime `DATABASE_URL` pointed at the connection string you use for serverless traffic.\n\n## External API v1\n\nThe external API exposes the configured user's planning data for project-owned consumers. Canonical endpoints live under `/api/external/v1/*`.\n\nThe authoritative machine-readable API reference is [`docs/openapi.yaml`](./docs/openapi.yaml). The examples below are a human-readable summary of that contract.\n\nEvery response from `/api/external/v1/*` includes an `X-Request-Id` header (UUID v4) for log correlation. The same ID is written to a structured JSON log line on the server alongside the request route, status, duration, outcome, and a non-sensitive 8-character prefix of the API key used. Consumers may capture this header to trace client-side errors back to server logs.\n\nServer-side errors are also captured to Sentry when the `SENTRY_DSN` environment variable is set. Captured events include `requestId`, `route`, and `outcome` tags so they can be correlated with the structured log lines emitted by the external API wrapper. Authorization headers are stripped before any event leaves the server.\n\nEvery response also includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (Unix epoch seconds) so consumers can self-throttle. When the limit is exceeded, the API returns 429 with the standard `Retry-After` header.\n\nEvery v1 response is JSON, dynamic (`force-dynamic`, `revalidate = 0`), and sent with `Cache-Control: no-store` and `X-Robots-Tag: noindex`.\n\n### Authentication\n\nEvery canonical v1 request must include the configured external key:\n\n```http\nAuthorization: Bearer \u003cEXTERNAL_API_KEY\u003e\n```\n\nKeys are compared with SHA-256 + `timingSafeEqual`. All external routes require `EXTERNAL_API_KEY` to be set.\n\n- Missing or malformed `Authorization` header → `401` JSON.\n- Wrong key → `403` JSON.\n- Required key is unset → `503` JSON.\n\nMost external API errors use this shape:\n\n```ts\ntype ExternalApiError = {\n  ok: false;\n  error: string;\n};\n```\n\n### Endpoints\n\n| Method | Path | Description |\n| --- | --- | --- |\n| `GET` | `/api/external/v1/dashboard` | Aggregate dashboard payload |\n| `GET` | `/api/external/v1/boards` | All boards owned by the configured external user |\n| `GET` | `/api/external/v1/boards/[slug]` | One board by slug, including tasks, subtasks, and note content |\n| `GET` | `/api/external/v1/daily-summary` | Daily briefing payload used by external automation |\n\n### `GET /api/external/v1/dashboard`\n\nRequest:\n\n```bash\ncurl -i \\\n  -H \"Authorization: Bearer $EXTERNAL_API_KEY\" \\\n  https://www.workflowblueprint.io/api/external/v1/dashboard\n```\n\nResponse:\n\n```ts\ntype ExternalDashboardResponse = {\n  ok: true;\n  data: {\n    boardBreakdown: Array\u003c{\n      slug: string;\n      name: string;\n      iconKey: string;\n      totalTasks: number;\n      percentage: number;\n    }\u003e;\n    sprintCompletionRate: number;\n    doneCount: number;\n    activeTaskCount: number;\n    inProgressCount: number;\n    closedLastSevenDays: number;\n    totalTaskCount: number;\n  };\n};\n```\n\n### `GET /api/external/v1/boards`\n\nRequest:\n\n```bash\ncurl -i \\\n  -H \"Authorization: Bearer $EXTERNAL_API_KEY\" \\\n  https://www.workflowblueprint.io/api/external/v1/boards\n```\n\nResponse:\n\n```ts\ntype ExternalBoardsResponse = {\n  ok: true;\n  data: {\n    boards: Array\u003c{\n      slug: string;\n      name: string;\n      description: string | null;\n      iconKey: string;\n      totalTasks: number;\n    }\u003e;\n  };\n};\n```\n\n### `GET /api/external/v1/boards/[slug]`\n\nRequest:\n\n```bash\ncurl -i \\\n  -H \"Authorization: Bearer $EXTERNAL_API_KEY\" \\\n  https://www.workflowblueprint.io/api/external/v1/boards/personal\n```\n\nResponse:\n\n```ts\ntype ExternalBoardResponse = {\n  ok: true;\n  data: {\n    id: string;\n    slug: string;\n    name: string;\n    description: string | null;\n    iconKey: string;\n    noteContent: string;\n    tasks: Array\u003c{\n      id: string;\n      title: string;\n      description: string | null;\n      status: \"ICE_BOX\" | \"ON_DECK\" | \"IN_PROGRESS\" | \"DONE\" | \"ARCHIVED\";\n      sortOrder: number;\n      priority: \"NONE\" | \"LOW\" | \"MEDIUM\" | \"HIGH\" | \"URGENT\";\n      dueDate: string | null;\n      completedAt: string | null;\n      archivedAt: string | null;\n      recurrence: \"NONE\" | \"DAILY\" | \"WEEKLY\" | \"MONTHLY\" | \"SEMI_ANNUALLY\" | \"ANNUALLY\";\n      subtasks: Array\u003c{\n        id: string;\n        title: string;\n        isComplete: boolean;\n        sortOrder: number;\n        priority: \"NONE\" | \"LOW\" | \"MEDIUM\" | \"HIGH\" | \"URGENT\";\n      }\u003e;\n    }\u003e;\n  };\n};\n```\n\n### `GET /api/external/v1/daily-summary`\n\nRequest:\n\n```bash\ncurl -i \\\n  -H \"Authorization: Bearer $EXTERNAL_API_KEY\" \\\n  https://www.workflowblueprint.io/api/external/v1/daily-summary\n```\n\nResponse:\n\n```ts\ntype ExternalDailySummaryResponse = {\n  generatedAt: string;\n  summary: {\n    totalActive: number;\n    completionRate: `${number}%`;\n    byStatus: {\n      iceBox: number;\n      onDeck: number;\n      inProgress: number;\n      done: number;\n      archived: number;\n    };\n    byCategory: Record\u003cstring, number\u003e;\n  };\n  inProgress: ExternalDailySummaryTask[];\n  onDeck: ExternalDailySummaryTask[];\n  iceBox: ExternalDailySummaryTask[];\n  recentlyCompleted: ExternalDailySummaryTask[];\n};\n\ntype ExternalDailySummaryTask = {\n  id: number;\n  title: string;\n  description: string | null;\n  status: \"ice-box\" | \"on-deck\" | \"in-progress\" | \"done\" | \"archived\";\n  category: string;\n  priority: \"none\" | \"low\" | \"medium\" | \"high\" | \"urgent\";\n  parentId: number | null;\n  sortOrder: number;\n  createdAt: string;\n  updatedAt: string;\n};\n```\n\nDaily-summary task ids are stable 48-bit hashes of the underlying UUIDs. `summary.byCategory` uses camelCase board slugs as keys.\n\n## Scripts\n\n```bash\nnpm run dev          # start the local Next.js server\nnpm run build        # local production build and type check (no migrations)\nnpm run vercel-build # Vercel uses this: applies Prisma migrations, then builds\nnpm run lint         # ESLint / Next core web vitals checks\nnpm run db:deploy    # apply checked-in Prisma migrations\nnpm run db:migrate   # create and apply a development migration\nnpm run db:push      # push schema directly for non-migration development\nnpm run db:seed      # seed the demo account and boards\n```\n\nVercel automatically runs `vercel-build` instead of `build` when it is present, so each production deployment applies any pending Prisma migrations before the new code starts handling requests. Local `npm run build` deliberately does not migrate so it cannot accidentally touch a remote database.\n\n## License\n\nThis project is licensed under the **PolyForm Noncommercial License 1.0.0**.\n\nThis is a source-available license that permits personal use, research, and non-commercial projects. **Commercial use is strictly prohibited without express written permission from Roy McFarland.**\n\nSee the [LICENSE](./LICENSE) file for the full text.\n\n## Security Notes\n\n- API routes use shared JSON parsing and Zod schema validation helpers.\n- External API responses are validated before being returned.\n- Authenticated API routes return JSON `401` responses instead of page redirects.\n- Sign-up, sign-in, password reset, invitation, and external API endpoints share a Postgres-backed distributed rate limiter (`RateLimitBucket` table) so limits hold across serverless instances.\n- Mutating routes verify the request `Origin`/`Referer` matches `NEXT_PUBLIC_SITE_URL` and the session cookie is `SameSite=strict`, providing a CSRF defense.\n- HTML responses get a per-request nonce-based Content Security Policy (`'strict-dynamic'`); API and static responses get a stricter baseline CSP.\n- Session JWTs include the user's `passwordChangedAt` timestamp so password changes/resets revoke every existing session.\n- Password reset and invitation tokens are stored hashed and claimed atomically inside transactions before any state changes.\n- Development reset links are returned only outside production; production sends reset and invitation links through Resend.\n- Admin actions (invitation create/revoke, role promotion) write an `AdminAuditLog` row recording actor, action, target, and timestamp.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froymcfarland%2Fworkflow-blueprint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froymcfarland%2Fworkflow-blueprint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froymcfarland%2Fworkflow-blueprint/lists"}