From 0af4ab367c892641ff036e652f4e1e5eab66893a Mon Sep 17 00:00:00 2001
From: joshuasilva414
Date: Tue, 27 Oct 2026 20:25:33 -0500
Subject: [PATCH 1/5] initial split
---
.github/workflows/apps.yml | 82 +
.github/workflows/integration.yml | 96 +
.github/workflows/web.yml | 132 -
.gitignore | 30 +-
.npmrc | 3 +
.prettierignore | 14 +-
.prettierrc | 2 +-
.tours/01-repository-map.tour | 72 +
.tours/02-core-startup.tour | 83 +
.tours/03-core-domain.tour | 119 +
.tours/04-plugin-system.tour | 101 +
.tours/05-cli-route-sync.tour | 59 +
.tours/06-request-flow.tour | 113 +
.tours/07-integrations-apps.tour | 107 +
.tours/08-verification.tour | 86 +
.tours/README.md | 26 +
README.md | 21 +-
apps/demo/.dev.vars.example | 6 +
apps/demo/.env.example | 1 +
apps/demo/README.md | 44 +
.../{web => demo}/app/admin/check-in/page.tsx | 0
.../app/admin/events/[id]/edit/page.tsx | 0
.../app/admin/events/new/page.tsx | 0
apps/{web => demo}/app/admin/events/page.tsx | 0
apps/{web => demo}/app/admin/layout.tsx | 0
apps/{web => demo}/app/admin/page.tsx | 0
apps/{web => demo}/app/admin/roles/page.tsx | 0
.../app/admin/scanner/[eventId]/page.tsx | 0
.../{web => demo}/app/admin/settings/page.tsx | 0
.../app/admin/users/[authId]/page.tsx | 0
apps/{web => demo}/app/admin/users/page.tsx | 0
.../app/api/admin/export/route.ts | 0
.../app/api/auth/[...all]/route.ts | 0
apps/demo/app/api/files/register/route.ts | 23 +
apps/demo/app/api/files/upload/route.ts | 36 +
apps/demo/app/api/files/view/route.ts | 54 +
.../app/api/hackkit/[...path]/route.ts | 0
apps/{web => demo}/app/app-bar.tsx | 0
apps/{web => demo}/app/contact/page.tsx | 0
apps/{web => demo}/app/dashboard/layout.tsx | 0
apps/{web => demo}/app/dashboard/page.tsx | 0
apps/demo/app/demo-actions.tsx | 84 +
apps/{web => demo}/app/globals.css | 0
apps/{web => demo}/app/help/page.tsx | 0
apps/{web => demo}/app/i/approval/layout.tsx | 0
apps/{web => demo}/app/i/approval/page.tsx | 0
apps/{web => demo}/app/invites/layout.tsx | 0
apps/{web => demo}/app/invites/page.tsx | 0
apps/demo/app/layout.tsx | 45 +
.../onboarding/hacker/hacker-form-defaults.ts | 0
.../hacker/hacker-registration-client.tsx | 0
.../app/onboarding/hacker/page.tsx | 0
.../app/onboarding/hacktag/page.tsx | 0
apps/{web => demo}/app/onboarding/layout.tsx | 0
.../app/onboarding/onboarding-shell.tsx | 0
.../app/onboarding/user-data/page.tsx | 0
apps/demo/app/page.tsx | 55 +
apps/{web => demo}/app/participant-layout.tsx | 0
.../app/pass/event-pass-shell.tsx | 0
apps/{web => demo}/app/pass/layout.tsx | 0
apps/{web => demo}/app/pass/page.tsx | 0
apps/{web => demo}/app/profile-menu.tsx | 1 +
apps/{web => demo}/app/providers.tsx | 0
apps/{web => demo}/app/register/page.tsx | 0
.../app/registration-closed/page.tsx | 0
apps/{web => demo}/app/rsvp/layout.tsx | 0
apps/{web => demo}/app/rsvp/page.tsx | 0
.../app/schedule/[eventId]/page.tsx | 0
apps/{web => demo}/app/schedule/page.tsx | 0
apps/{web => demo}/app/settings/layout.tsx | 0
apps/{web => demo}/app/settings/page.tsx | 0
.../app/settings/profile-settings-client.tsx | 0
.../app/settings/registration/page.tsx | 0
apps/{web => demo}/app/sign-in/auth-form.tsx | 0
apps/{web => demo}/app/sign-in/page.tsx | 4 +-
apps/{web => demo}/app/sign-up/page.tsx | 4 +-
apps/{web => demo}/app/suspended/page.tsx | 0
apps/{web => demo}/app/teams/layout.tsx | 0
apps/{web => demo}/app/teams/page.tsx | 0
apps/{web => demo}/app/user/[tag]/page.tsx | 0
apps/demo/db/migrations/sqlite/.snapshot.json | 3800 ++++
.../Migration20261020221614_demo_initial.ts | 258 +
apps/demo/env.ts | 25 +
apps/demo/hackkit.config.ts | 138 +
apps/demo/hackkit.lock | 20 +
apps/demo/lib/app-config.ts | 15 +
apps/{web => demo}/lib/auth-client.ts | 0
apps/demo/lib/auth-options.ts | 8 +
apps/{web => demo}/lib/auth.ts | 0
apps/demo/lib/blob.ts | 65 +
apps/demo/lib/database-config.ts | 16 +
apps/demo/lib/demo-accounts.ts | 20 +
apps/{web => demo}/lib/hackkit-ui-routes.ts | 0
apps/demo/lib/http-libsql-dialect.ts | 66 +
apps/{web => demo}/lib/logger.ts | 0
apps/{web => demo}/lib/onboarding.ts | 0
apps/{web => demo}/lib/plugin-navigation.ts | 0
apps/demo/lib/public-site-config.ts | 249 +
apps/demo/lib/runtime.ts | 43 +
.../{web => demo}/lib/upload-profile-photo.ts | 0
apps/{web => demo}/lib/upload-resume.ts | 0
apps/{web => demo}/middleware.ts | 0
apps/demo/next-env.d.ts | 7 +
apps/demo/next.config.mjs | 2 +
apps/demo/package.json | 72 +
apps/demo/playwright.config.ts | 31 +
apps/{web => demo}/postcss.config.cjs | 0
apps/demo/scripts/build.mjs | 20 +
apps/demo/scripts/compile-orm.mjs | 24 +
apps/demo/scripts/database.ts | 120 +
apps/demo/scripts/db.mjs | 62 +
apps/demo/scripts/orm.config.mjs | 5 +
apps/demo/scripts/seed.mjs | 9 +
apps/demo/scripts/seed.ts | 101 +
apps/demo/scripts/test-database.mjs | 56 +
apps/demo/tailwind.config.ts | 60 +
apps/demo/tests/demo.spec.ts | 235 +
apps/demo/tsconfig.json | 44 +
apps/demo/tsconfig.typecheck.json | 22 +
apps/demo/vite.config.ts | 21 +
apps/demo/worker/env.d.ts | 43 +
apps/demo/wrangler.jsonc | 15 +
apps/docs/.gitignore | 26 +
apps/docs/README.md | 16 +
apps/docs/app/(home)/layout.tsx | 6 +
apps/docs/app/(home)/page.tsx | 76 +
apps/docs/app/api/search/route.ts | 9 +
apps/docs/app/docs/[[...slug]]/page.tsx | 64 +
apps/docs/app/docs/layout.tsx | 11 +
apps/docs/app/global.css | 12 +
apps/docs/app/layout.tsx | 18 +
apps/docs/app/llms-full.txt/route.ts | 7 +
.../app/llms.mdx/docs/[[...slug]]/route.ts | 27 +
apps/docs/app/llms.txt/route.ts | 7 +
apps/docs/app/og/docs/[...slug]/route.tsx | 28 +
apps/docs/components/mdx.tsx | 15 +
apps/docs/components/provider.tsx | 8 +
apps/docs/components/search.tsx | 35 +
apps/docs/content/docs/administration.mdx | 20 +
apps/docs/content/docs/architecture.mdx | 27 +
apps/docs/content/docs/configuration.mdx | 22 +
apps/docs/content/docs/database.mdx | 31 +
apps/docs/content/docs/demo.mdx | 32 +
apps/docs/content/docs/deployment.mdx | 37 +
apps/docs/content/docs/getting-started.mdx | 38 +
apps/docs/content/docs/index.mdx | 25 +
apps/docs/content/docs/meta.json | 16 +
apps/docs/content/docs/plugins.mdx | 30 +
apps/docs/content/docs/templates.mdx | 23 +
apps/docs/content/docs/testing.mdx | 28 +
apps/docs/lib/cn.ts | 1 +
apps/docs/lib/layout.shared.tsx | 12 +
apps/docs/lib/shared.ts | 29 +
apps/docs/lib/source.ts | 30 +
apps/docs/next.config.mjs | 11 +
apps/docs/package.json | 35 +
apps/docs/postcss.config.mjs | 7 +
apps/docs/tsconfig.json | 34 +
apps/docs/wrangler.jsonc | 12 +
apps/web/scripts/test-db-workflow.sh | 54 -
apps/web/scripts/test-runtime.mjs | 236 -
docs/guides/database-migrations.md | 32 +-
docs/guides/next-integration.md | 4 +-
docs/plans/template-distribution.md | 16 +
e2e/README.md | 37 +
e2e/browser/participant.spec.ts | 95 +
e2e/browser/teams.spec.ts | 56 +
.../database}/database-workflow.test.ts | 7 +-
.../database}/dialects.test.ts | 7 +-
.../database}/migration-fixture-schema.ts | 0
.../database}/migration-workflow.test.ts | 12 +-
.../database}/migration-workflow.ts | 13 +-
.../database}/profile-concurrency.ts | 2 +-
e2e/database/templates.test.ts | 138 +
e2e/http/runtime.spec.ts | 215 +
e2e/package.json | 33 +
e2e/playwright.config.ts | 30 +
e2e/support/accounts.ts | 96 +
e2e/support/browser.ts | 85 +
e2e/support/database-env.ts | 10 +
e2e/support/database-task.mjs | 34 +
e2e/support/fixtures.ts | 57 +
e2e/support/processes.mjs | 50 +
e2e/support/run.mjs | 100 +
e2e/support/server.mjs | 108 +
e2e/support/templates.mjs | 72 +
e2e/tsconfig.json | 10 +
e2e/vitest.config.ts | 26 +
package.json | 38 +-
packages/config/src/__tests__/config.test.ts | 26 +-
.../src/__tests__/fixtures/hackkit.config.ts | 5 +
packages/config/src/index.ts | 5 +-
packages/core/package.json | 3 +-
packages/core/src/mikro/options.ts | 2 +
packages/next/README.md | 2 +-
packages/plugin-teams/package.json | 3 +
.../src/components/team-dashboard.tsx | 2 +-
packages/ui/package.json | 3 +
.../src/components/admin-console-controls.tsx | 189 +-
packages/ui/src/components/admin-console.tsx | 100 +-
.../components/hacker-registration-form.tsx | 5 +-
packages/ui/src/components/public-shell.tsx | 16 +-
packages/ui/src/components/user-data-form.tsx | 5 +-
.../components/user-profile-settings-form.tsx | 39 +-
pnpm-lock.yaml | 15526 +++++++++++-----
pnpm-workspace.yaml | 20 +
templates/nextjs-full/.env.example | 64 +
{apps/web => templates/nextjs-full}/README.md | 33 +-
.../nextjs-full/app/admin/check-in/page.tsx | 30 +
.../app/admin/events/[id]/edit/page.tsx | 49 +
.../nextjs-full/app/admin/events/new/page.tsx | 29 +
.../nextjs-full/app/admin/events/page.tsx | 44 +
templates/nextjs-full/app/admin/layout.tsx | 50 +
templates/nextjs-full/app/admin/page.tsx | 24 +
.../nextjs-full/app/admin/roles/page.tsx | 26 +
.../app/admin/scanner/[eventId]/page.tsx | 44 +
.../nextjs-full/app/admin/settings/page.tsx | 28 +
.../app/admin/users/[authId]/page.tsx | 36 +
.../nextjs-full/app/admin/users/page.tsx | 23 +
.../nextjs-full/app/api/admin/export/route.ts | 57 +
.../app/api/auth/[...all]/route.ts | 9 +
.../app/api/files/register/route.ts | 0
.../app/api/files/upload/route.ts | 0
.../nextjs-full}/app/api/files/view/route.ts | 0
.../app/api/hackkit/[...path]/route.ts | 4 +
templates/nextjs-full/app/app-bar.tsx | 80 +
templates/nextjs-full/app/contact/page.tsx | 1 +
.../nextjs-full/app/dashboard/layout.tsx | 1 +
templates/nextjs-full/app/dashboard/page.tsx | 71 +
.../nextjs-full}/app/discord/page.tsx | 0
.../nextjs-full}/app/discord/verify/page.tsx | 0
templates/nextjs-full/app/globals.css | 58 +
templates/nextjs-full/app/help/page.tsx | 13 +
.../nextjs-full/app/i/approval/layout.tsx | 14 +
templates/nextjs-full/app/i/approval/page.tsx | 27 +
templates/nextjs-full/app/invites/layout.tsx | 1 +
templates/nextjs-full/app/invites/page.tsx | 4 +
.../nextjs-full}/app/layout.tsx | 0
.../onboarding/hacker/hacker-form-defaults.ts | 18 +
.../hacker/hacker-registration-client.tsx | 45 +
.../app/onboarding/hacker/page.tsx | 40 +
.../app/onboarding/hacktag/page.tsx | 30 +
.../nextjs-full/app/onboarding/layout.tsx | 14 +
.../app/onboarding/onboarding-shell.tsx | 35 +
.../app/onboarding/user-data/page.tsx | 38 +
.../nextjs-full}/app/page.tsx | 0
.../nextjs-full/app/participant-layout.tsx | 14 +
.../nextjs-full/app/pass/event-pass-shell.tsx | 42 +
templates/nextjs-full/app/pass/layout.tsx | 1 +
templates/nextjs-full/app/pass/page.tsx | 22 +
templates/nextjs-full/app/profile-menu.tsx | 116 +
templates/nextjs-full/app/providers.tsx | 12 +
templates/nextjs-full/app/register/page.tsx | 5 +
.../app/registration-closed/page.tsx | 26 +
templates/nextjs-full/app/rsvp/layout.tsx | 1 +
templates/nextjs-full/app/rsvp/page.tsx | 19 +
.../app/schedule/[eventId]/page.tsx | 37 +
templates/nextjs-full/app/schedule/page.tsx | 29 +
templates/nextjs-full/app/settings/layout.tsx | 1 +
templates/nextjs-full/app/settings/page.tsx | 53 +
.../app/settings/profile-settings-client.tsx | 14 +
.../app/settings/registration/page.tsx | 54 +
.../nextjs-full/app/sign-in/auth-form.tsx | 100 +
templates/nextjs-full/app/sign-in/page.tsx | 24 +
templates/nextjs-full/app/sign-up/page.tsx | 27 +
templates/nextjs-full/app/suspended/page.tsx | 18 +
templates/nextjs-full/app/teams/layout.tsx | 1 +
templates/nextjs-full/app/teams/page.tsx | 4 +
templates/nextjs-full/app/user/[tag]/page.tsx | 23 +
.../db/migrations/mysql/.snapshot.json | 0
.../mysql/Migration20260927180400_initial.ts | 0
.../db/migrations/postgresql/.snapshot.json | 0
.../Migration20260927180400_initial.ts | 0
.../db/migrations/sqlite/.snapshot.json | 0
.../sqlite/Migration20260927180400_initial.ts | 0
{apps/web => templates/nextjs-full}/env.ts | 0
.../nextjs-full}/hackkit.config.ts | 0
.../nextjs-full}/hackkit.lock | 0
.../nextjs-full}/lib/app-config.ts | 0
templates/nextjs-full/lib/auth-client.ts | 5 +
.../nextjs-full}/lib/auth-options.ts | 0
templates/nextjs-full/lib/auth.ts | 11 +
.../web => templates/nextjs-full}/lib/blob.ts | 0
.../nextjs-full}/lib/database-config.ts | 0
.../nextjs-full/lib/hackkit-ui-routes.ts | 7 +
templates/nextjs-full/lib/logger.ts | 11 +
templates/nextjs-full/lib/onboarding.ts | 54 +
.../nextjs-full/lib/plugin-navigation.ts | 20 +
.../nextjs-full}/lib/public-site-config.ts | 0
.../nextjs-full}/lib/runtime.ts | 0
.../nextjs-full/lib/upload-profile-photo.ts | 34 +
templates/nextjs-full/lib/upload-resume.ts | 59 +
templates/nextjs-full/middleware.ts | 72 +
.../nextjs-full}/next-env.d.ts | 0
.../nextjs-full}/next.config.mjs | 0
.../nextjs-full}/package.json | 12 +-
templates/nextjs-full/postcss.config.cjs | 6 +
.../nextjs-full}/scripts/database.ts | 0
.../nextjs-full}/scripts/db.mjs | 0
templates/nextjs-full/tailwind.config.ts | 60 +
.../nextjs-full}/tsconfig.json | 0
templates/nextjs-minimal/.env.example | 18 +
templates/nextjs-minimal/README.md | 38 +
templates/nextjs-minimal/app/admin/layout.tsx | 48 +
templates/nextjs-minimal/app/admin/page.tsx | 27 +
.../nextjs-minimal/app/admin/roles/page.tsx | 26 +
.../app/admin/settings/page.tsx | 38 +
.../app/admin/users/[authId]/page.tsx | 40 +
.../nextjs-minimal/app/admin/users/page.tsx | 26 +
.../app/api/admin/export/route.ts | 57 +
.../app/api/auth/[...all]/route.ts | 9 +
.../app/api/files/register/route.ts | 41 +
.../app/api/files/upload/route.ts | 30 +
.../app/api/files/view/route.ts | 55 +
.../app/api/hackkit/[...path]/route.ts | 4 +
templates/nextjs-minimal/app/app-bar.tsx | 79 +
templates/nextjs-minimal/app/contact/page.tsx | 1 +
.../nextjs-minimal/app/dashboard/layout.tsx | 1 +
.../nextjs-minimal/app/dashboard/page.tsx | 68 +
templates/nextjs-minimal/app/globals.css | 58 +
templates/nextjs-minimal/app/help/page.tsx | 13 +
.../nextjs-minimal/app/i/approval/layout.tsx | 14 +
.../nextjs-minimal/app/i/approval/page.tsx | 26 +
templates/nextjs-minimal/app/layout.tsx | 38 +
.../onboarding/hacker/hacker-form-defaults.ts | 18 +
.../hacker/hacker-registration-client.tsx | 45 +
.../app/onboarding/hacker/page.tsx | 40 +
.../app/onboarding/hacktag/page.tsx | 30 +
.../nextjs-minimal/app/onboarding/layout.tsx | 14 +
.../app/onboarding/onboarding-shell.tsx | 35 +
.../app/onboarding/user-data/page.tsx | 38 +
templates/nextjs-minimal/app/page.tsx | 17 +
.../nextjs-minimal/app/participant-layout.tsx | 14 +
templates/nextjs-minimal/app/profile-menu.tsx | 116 +
templates/nextjs-minimal/app/providers.tsx | 12 +
.../nextjs-minimal/app/register/page.tsx | 5 +
.../app/registration-closed/page.tsx | 26 +
.../nextjs-minimal/app/settings/layout.tsx | 1 +
.../nextjs-minimal/app/settings/page.tsx | 36 +
.../app/settings/profile-settings-client.tsx | 15 +
.../app/settings/registration/page.tsx | 54 +
.../nextjs-minimal/app/sign-in/auth-form.tsx | 100 +
templates/nextjs-minimal/app/sign-in/page.tsx | 24 +
templates/nextjs-minimal/app/sign-up/page.tsx | 27 +
.../nextjs-minimal/app/suspended/page.tsx | 18 +
.../nextjs-minimal/app/user/[tag]/page.tsx | 23 +
.../db/migrations/sqlite/.snapshot.json | 3334 ++++
.../sqlite/Migration20261020200349_initial.ts | 101 +
templates/nextjs-minimal/env.ts | 49 +
templates/nextjs-minimal/hackkit.config.ts | 105 +
templates/nextjs-minimal/hackkit.lock | 4 +
templates/nextjs-minimal/lib/app-config.ts | 11 +
templates/nextjs-minimal/lib/auth-client.ts | 5 +
templates/nextjs-minimal/lib/auth-options.ts | 8 +
templates/nextjs-minimal/lib/auth.ts | 11 +
templates/nextjs-minimal/lib/blob.ts | 11 +
.../nextjs-minimal/lib/database-config.ts | 19 +
.../nextjs-minimal/lib/hackkit-ui-routes.ts | 7 +
templates/nextjs-minimal/lib/logger.ts | 11 +
templates/nextjs-minimal/lib/onboarding.ts | 54 +
.../nextjs-minimal/lib/public-site-config.ts | 122 +
templates/nextjs-minimal/lib/runtime.ts | 35 +
.../lib/upload-profile-photo.ts | 34 +
templates/nextjs-minimal/lib/upload-resume.ts | 59 +
templates/nextjs-minimal/middleware.ts | 68 +
templates/nextjs-minimal/next-env.d.ts | 5 +
templates/nextjs-minimal/next.config.mjs | 20 +
templates/nextjs-minimal/package.json | 50 +
templates/nextjs-minimal/postcss.config.cjs | 6 +
templates/nextjs-minimal/scripts/database.ts | 146 +
templates/nextjs-minimal/scripts/db.mjs | 66 +
.../nextjs-minimal}/tailwind.config.ts | 0
templates/nextjs-minimal/tsconfig.json | 23 +
tooling/check-client-bundle.mjs | 68 +-
turbo.json | 203 +-
375 files changed, 28151 insertions(+), 5645 deletions(-)
create mode 100644 .github/workflows/apps.yml
create mode 100644 .github/workflows/integration.yml
delete mode 100644 .github/workflows/web.yml
create mode 100644 .tours/01-repository-map.tour
create mode 100644 .tours/02-core-startup.tour
create mode 100644 .tours/03-core-domain.tour
create mode 100644 .tours/04-plugin-system.tour
create mode 100644 .tours/05-cli-route-sync.tour
create mode 100644 .tours/06-request-flow.tour
create mode 100644 .tours/07-integrations-apps.tour
create mode 100644 .tours/08-verification.tour
create mode 100644 .tours/README.md
create mode 100644 apps/demo/.dev.vars.example
create mode 100644 apps/demo/.env.example
create mode 100644 apps/demo/README.md
rename apps/{web => demo}/app/admin/check-in/page.tsx (100%)
rename apps/{web => demo}/app/admin/events/[id]/edit/page.tsx (100%)
rename apps/{web => demo}/app/admin/events/new/page.tsx (100%)
rename apps/{web => demo}/app/admin/events/page.tsx (100%)
rename apps/{web => demo}/app/admin/layout.tsx (100%)
rename apps/{web => demo}/app/admin/page.tsx (100%)
rename apps/{web => demo}/app/admin/roles/page.tsx (100%)
rename apps/{web => demo}/app/admin/scanner/[eventId]/page.tsx (100%)
rename apps/{web => demo}/app/admin/settings/page.tsx (100%)
rename apps/{web => demo}/app/admin/users/[authId]/page.tsx (100%)
rename apps/{web => demo}/app/admin/users/page.tsx (100%)
rename apps/{web => demo}/app/api/admin/export/route.ts (100%)
rename apps/{web => demo}/app/api/auth/[...all]/route.ts (100%)
create mode 100644 apps/demo/app/api/files/register/route.ts
create mode 100644 apps/demo/app/api/files/upload/route.ts
create mode 100644 apps/demo/app/api/files/view/route.ts
rename apps/{web => demo}/app/api/hackkit/[...path]/route.ts (100%)
rename apps/{web => demo}/app/app-bar.tsx (100%)
rename apps/{web => demo}/app/contact/page.tsx (100%)
rename apps/{web => demo}/app/dashboard/layout.tsx (100%)
rename apps/{web => demo}/app/dashboard/page.tsx (100%)
create mode 100644 apps/demo/app/demo-actions.tsx
rename apps/{web => demo}/app/globals.css (100%)
rename apps/{web => demo}/app/help/page.tsx (100%)
rename apps/{web => demo}/app/i/approval/layout.tsx (100%)
rename apps/{web => demo}/app/i/approval/page.tsx (100%)
rename apps/{web => demo}/app/invites/layout.tsx (100%)
rename apps/{web => demo}/app/invites/page.tsx (100%)
create mode 100644 apps/demo/app/layout.tsx
rename apps/{web => demo}/app/onboarding/hacker/hacker-form-defaults.ts (100%)
rename apps/{web => demo}/app/onboarding/hacker/hacker-registration-client.tsx (100%)
rename apps/{web => demo}/app/onboarding/hacker/page.tsx (100%)
rename apps/{web => demo}/app/onboarding/hacktag/page.tsx (100%)
rename apps/{web => demo}/app/onboarding/layout.tsx (100%)
rename apps/{web => demo}/app/onboarding/onboarding-shell.tsx (100%)
rename apps/{web => demo}/app/onboarding/user-data/page.tsx (100%)
create mode 100644 apps/demo/app/page.tsx
rename apps/{web => demo}/app/participant-layout.tsx (100%)
rename apps/{web => demo}/app/pass/event-pass-shell.tsx (100%)
rename apps/{web => demo}/app/pass/layout.tsx (100%)
rename apps/{web => demo}/app/pass/page.tsx (100%)
rename apps/{web => demo}/app/profile-menu.tsx (98%)
rename apps/{web => demo}/app/providers.tsx (100%)
rename apps/{web => demo}/app/register/page.tsx (100%)
rename apps/{web => demo}/app/registration-closed/page.tsx (100%)
rename apps/{web => demo}/app/rsvp/layout.tsx (100%)
rename apps/{web => demo}/app/rsvp/page.tsx (100%)
rename apps/{web => demo}/app/schedule/[eventId]/page.tsx (100%)
rename apps/{web => demo}/app/schedule/page.tsx (100%)
rename apps/{web => demo}/app/settings/layout.tsx (100%)
rename apps/{web => demo}/app/settings/page.tsx (100%)
rename apps/{web => demo}/app/settings/profile-settings-client.tsx (100%)
rename apps/{web => demo}/app/settings/registration/page.tsx (100%)
rename apps/{web => demo}/app/sign-in/auth-form.tsx (100%)
rename apps/{web => demo}/app/sign-in/page.tsx (86%)
rename apps/{web => demo}/app/sign-up/page.tsx (89%)
rename apps/{web => demo}/app/suspended/page.tsx (100%)
rename apps/{web => demo}/app/teams/layout.tsx (100%)
rename apps/{web => demo}/app/teams/page.tsx (100%)
rename apps/{web => demo}/app/user/[tag]/page.tsx (100%)
create mode 100644 apps/demo/db/migrations/sqlite/.snapshot.json
create mode 100644 apps/demo/db/migrations/sqlite/Migration20261020221614_demo_initial.ts
create mode 100644 apps/demo/env.ts
create mode 100644 apps/demo/hackkit.config.ts
create mode 100644 apps/demo/hackkit.lock
create mode 100644 apps/demo/lib/app-config.ts
rename apps/{web => demo}/lib/auth-client.ts (100%)
create mode 100644 apps/demo/lib/auth-options.ts
rename apps/{web => demo}/lib/auth.ts (100%)
create mode 100644 apps/demo/lib/blob.ts
create mode 100644 apps/demo/lib/database-config.ts
create mode 100644 apps/demo/lib/demo-accounts.ts
rename apps/{web => demo}/lib/hackkit-ui-routes.ts (100%)
create mode 100644 apps/demo/lib/http-libsql-dialect.ts
rename apps/{web => demo}/lib/logger.ts (100%)
rename apps/{web => demo}/lib/onboarding.ts (100%)
rename apps/{web => demo}/lib/plugin-navigation.ts (100%)
create mode 100644 apps/demo/lib/public-site-config.ts
create mode 100644 apps/demo/lib/runtime.ts
rename apps/{web => demo}/lib/upload-profile-photo.ts (100%)
rename apps/{web => demo}/lib/upload-resume.ts (100%)
rename apps/{web => demo}/middleware.ts (100%)
create mode 100644 apps/demo/next-env.d.ts
create mode 100644 apps/demo/next.config.mjs
create mode 100644 apps/demo/package.json
create mode 100644 apps/demo/playwright.config.ts
rename apps/{web => demo}/postcss.config.cjs (100%)
create mode 100644 apps/demo/scripts/build.mjs
create mode 100644 apps/demo/scripts/compile-orm.mjs
create mode 100644 apps/demo/scripts/database.ts
create mode 100644 apps/demo/scripts/db.mjs
create mode 100644 apps/demo/scripts/orm.config.mjs
create mode 100644 apps/demo/scripts/seed.mjs
create mode 100644 apps/demo/scripts/seed.ts
create mode 100644 apps/demo/scripts/test-database.mjs
create mode 100644 apps/demo/tailwind.config.ts
create mode 100644 apps/demo/tests/demo.spec.ts
create mode 100644 apps/demo/tsconfig.json
create mode 100644 apps/demo/tsconfig.typecheck.json
create mode 100644 apps/demo/vite.config.ts
create mode 100644 apps/demo/worker/env.d.ts
create mode 100644 apps/demo/wrangler.jsonc
create mode 100644 apps/docs/.gitignore
create mode 100644 apps/docs/README.md
create mode 100644 apps/docs/app/(home)/layout.tsx
create mode 100644 apps/docs/app/(home)/page.tsx
create mode 100644 apps/docs/app/api/search/route.ts
create mode 100644 apps/docs/app/docs/[[...slug]]/page.tsx
create mode 100644 apps/docs/app/docs/layout.tsx
create mode 100644 apps/docs/app/global.css
create mode 100644 apps/docs/app/layout.tsx
create mode 100644 apps/docs/app/llms-full.txt/route.ts
create mode 100644 apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts
create mode 100644 apps/docs/app/llms.txt/route.ts
create mode 100644 apps/docs/app/og/docs/[...slug]/route.tsx
create mode 100644 apps/docs/components/mdx.tsx
create mode 100644 apps/docs/components/provider.tsx
create mode 100644 apps/docs/components/search.tsx
create mode 100644 apps/docs/content/docs/administration.mdx
create mode 100644 apps/docs/content/docs/architecture.mdx
create mode 100644 apps/docs/content/docs/configuration.mdx
create mode 100644 apps/docs/content/docs/database.mdx
create mode 100644 apps/docs/content/docs/demo.mdx
create mode 100644 apps/docs/content/docs/deployment.mdx
create mode 100644 apps/docs/content/docs/getting-started.mdx
create mode 100644 apps/docs/content/docs/index.mdx
create mode 100644 apps/docs/content/docs/meta.json
create mode 100644 apps/docs/content/docs/plugins.mdx
create mode 100644 apps/docs/content/docs/templates.mdx
create mode 100644 apps/docs/content/docs/testing.mdx
create mode 100644 apps/docs/lib/cn.ts
create mode 100644 apps/docs/lib/layout.shared.tsx
create mode 100644 apps/docs/lib/shared.ts
create mode 100644 apps/docs/lib/source.ts
create mode 100644 apps/docs/next.config.mjs
create mode 100644 apps/docs/package.json
create mode 100644 apps/docs/postcss.config.mjs
create mode 100644 apps/docs/tsconfig.json
create mode 100644 apps/docs/wrangler.jsonc
delete mode 100644 apps/web/scripts/test-db-workflow.sh
delete mode 100644 apps/web/scripts/test-runtime.mjs
create mode 100644 docs/plans/template-distribution.md
create mode 100644 e2e/README.md
create mode 100644 e2e/browser/participant.spec.ts
create mode 100644 e2e/browser/teams.spec.ts
rename {apps/web/db/__tests__ => e2e/database}/database-workflow.test.ts (88%)
rename {apps/web/db/__tests__ => e2e/database}/dialects.test.ts (98%)
rename {apps/web/db/__tests__ => e2e/database}/migration-fixture-schema.ts (100%)
rename {apps/web/db/__tests__ => e2e/database}/migration-workflow.test.ts (89%)
rename {apps/web/db/__tests__ => e2e/database}/migration-workflow.ts (96%)
rename {apps/web/db/__tests__ => e2e/database}/profile-concurrency.ts (98%)
create mode 100644 e2e/database/templates.test.ts
create mode 100644 e2e/http/runtime.spec.ts
create mode 100644 e2e/package.json
create mode 100644 e2e/playwright.config.ts
create mode 100644 e2e/support/accounts.ts
create mode 100644 e2e/support/browser.ts
create mode 100644 e2e/support/database-env.ts
create mode 100644 e2e/support/database-task.mjs
create mode 100644 e2e/support/fixtures.ts
create mode 100644 e2e/support/processes.mjs
create mode 100644 e2e/support/run.mjs
create mode 100644 e2e/support/server.mjs
create mode 100644 e2e/support/templates.mjs
create mode 100644 e2e/tsconfig.json
create mode 100644 e2e/vitest.config.ts
create mode 100644 packages/config/src/__tests__/fixtures/hackkit.config.ts
create mode 100644 templates/nextjs-full/.env.example
rename {apps/web => templates/nextjs-full}/README.md (67%)
create mode 100644 templates/nextjs-full/app/admin/check-in/page.tsx
create mode 100644 templates/nextjs-full/app/admin/events/[id]/edit/page.tsx
create mode 100644 templates/nextjs-full/app/admin/events/new/page.tsx
create mode 100644 templates/nextjs-full/app/admin/events/page.tsx
create mode 100644 templates/nextjs-full/app/admin/layout.tsx
create mode 100644 templates/nextjs-full/app/admin/page.tsx
create mode 100644 templates/nextjs-full/app/admin/roles/page.tsx
create mode 100644 templates/nextjs-full/app/admin/scanner/[eventId]/page.tsx
create mode 100644 templates/nextjs-full/app/admin/settings/page.tsx
create mode 100644 templates/nextjs-full/app/admin/users/[authId]/page.tsx
create mode 100644 templates/nextjs-full/app/admin/users/page.tsx
create mode 100644 templates/nextjs-full/app/api/admin/export/route.ts
create mode 100644 templates/nextjs-full/app/api/auth/[...all]/route.ts
rename {apps/web => templates/nextjs-full}/app/api/files/register/route.ts (100%)
rename {apps/web => templates/nextjs-full}/app/api/files/upload/route.ts (100%)
rename {apps/web => templates/nextjs-full}/app/api/files/view/route.ts (100%)
create mode 100644 templates/nextjs-full/app/api/hackkit/[...path]/route.ts
create mode 100644 templates/nextjs-full/app/app-bar.tsx
create mode 100644 templates/nextjs-full/app/contact/page.tsx
create mode 100644 templates/nextjs-full/app/dashboard/layout.tsx
create mode 100644 templates/nextjs-full/app/dashboard/page.tsx
rename {apps/web => templates/nextjs-full}/app/discord/page.tsx (100%)
rename {apps/web => templates/nextjs-full}/app/discord/verify/page.tsx (100%)
create mode 100644 templates/nextjs-full/app/globals.css
create mode 100644 templates/nextjs-full/app/help/page.tsx
create mode 100644 templates/nextjs-full/app/i/approval/layout.tsx
create mode 100644 templates/nextjs-full/app/i/approval/page.tsx
create mode 100644 templates/nextjs-full/app/invites/layout.tsx
create mode 100644 templates/nextjs-full/app/invites/page.tsx
rename {apps/web => templates/nextjs-full}/app/layout.tsx (100%)
create mode 100644 templates/nextjs-full/app/onboarding/hacker/hacker-form-defaults.ts
create mode 100644 templates/nextjs-full/app/onboarding/hacker/hacker-registration-client.tsx
create mode 100644 templates/nextjs-full/app/onboarding/hacker/page.tsx
create mode 100644 templates/nextjs-full/app/onboarding/hacktag/page.tsx
create mode 100644 templates/nextjs-full/app/onboarding/layout.tsx
create mode 100644 templates/nextjs-full/app/onboarding/onboarding-shell.tsx
create mode 100644 templates/nextjs-full/app/onboarding/user-data/page.tsx
rename {apps/web => templates/nextjs-full}/app/page.tsx (100%)
create mode 100644 templates/nextjs-full/app/participant-layout.tsx
create mode 100644 templates/nextjs-full/app/pass/event-pass-shell.tsx
create mode 100644 templates/nextjs-full/app/pass/layout.tsx
create mode 100644 templates/nextjs-full/app/pass/page.tsx
create mode 100644 templates/nextjs-full/app/profile-menu.tsx
create mode 100644 templates/nextjs-full/app/providers.tsx
create mode 100644 templates/nextjs-full/app/register/page.tsx
create mode 100644 templates/nextjs-full/app/registration-closed/page.tsx
create mode 100644 templates/nextjs-full/app/rsvp/layout.tsx
create mode 100644 templates/nextjs-full/app/rsvp/page.tsx
create mode 100644 templates/nextjs-full/app/schedule/[eventId]/page.tsx
create mode 100644 templates/nextjs-full/app/schedule/page.tsx
create mode 100644 templates/nextjs-full/app/settings/layout.tsx
create mode 100644 templates/nextjs-full/app/settings/page.tsx
create mode 100644 templates/nextjs-full/app/settings/profile-settings-client.tsx
create mode 100644 templates/nextjs-full/app/settings/registration/page.tsx
create mode 100644 templates/nextjs-full/app/sign-in/auth-form.tsx
create mode 100644 templates/nextjs-full/app/sign-in/page.tsx
create mode 100644 templates/nextjs-full/app/sign-up/page.tsx
create mode 100644 templates/nextjs-full/app/suspended/page.tsx
create mode 100644 templates/nextjs-full/app/teams/layout.tsx
create mode 100644 templates/nextjs-full/app/teams/page.tsx
create mode 100644 templates/nextjs-full/app/user/[tag]/page.tsx
rename {apps/web => templates/nextjs-full}/db/migrations/mysql/.snapshot.json (100%)
rename {apps/web => templates/nextjs-full}/db/migrations/mysql/Migration20260927180400_initial.ts (100%)
rename {apps/web => templates/nextjs-full}/db/migrations/postgresql/.snapshot.json (100%)
rename {apps/web => templates/nextjs-full}/db/migrations/postgresql/Migration20260927180400_initial.ts (100%)
rename {apps/web => templates/nextjs-full}/db/migrations/sqlite/.snapshot.json (100%)
rename {apps/web => templates/nextjs-full}/db/migrations/sqlite/Migration20260927180400_initial.ts (100%)
rename {apps/web => templates/nextjs-full}/env.ts (100%)
rename {apps/web => templates/nextjs-full}/hackkit.config.ts (100%)
rename {apps/web => templates/nextjs-full}/hackkit.lock (100%)
rename {apps/web => templates/nextjs-full}/lib/app-config.ts (100%)
create mode 100644 templates/nextjs-full/lib/auth-client.ts
rename {apps/web => templates/nextjs-full}/lib/auth-options.ts (100%)
create mode 100644 templates/nextjs-full/lib/auth.ts
rename {apps/web => templates/nextjs-full}/lib/blob.ts (100%)
rename {apps/web => templates/nextjs-full}/lib/database-config.ts (100%)
create mode 100644 templates/nextjs-full/lib/hackkit-ui-routes.ts
create mode 100644 templates/nextjs-full/lib/logger.ts
create mode 100644 templates/nextjs-full/lib/onboarding.ts
create mode 100644 templates/nextjs-full/lib/plugin-navigation.ts
rename {apps/web => templates/nextjs-full}/lib/public-site-config.ts (100%)
rename {apps/web => templates/nextjs-full}/lib/runtime.ts (100%)
create mode 100644 templates/nextjs-full/lib/upload-profile-photo.ts
create mode 100644 templates/nextjs-full/lib/upload-resume.ts
create mode 100644 templates/nextjs-full/middleware.ts
rename {apps/web => templates/nextjs-full}/next-env.d.ts (100%)
rename {apps/web => templates/nextjs-full}/next.config.mjs (100%)
rename {apps/web => templates/nextjs-full}/package.json (77%)
create mode 100644 templates/nextjs-full/postcss.config.cjs
rename {apps/web => templates/nextjs-full}/scripts/database.ts (100%)
rename {apps/web => templates/nextjs-full}/scripts/db.mjs (100%)
create mode 100644 templates/nextjs-full/tailwind.config.ts
rename {apps/web => templates/nextjs-full}/tsconfig.json (100%)
create mode 100644 templates/nextjs-minimal/.env.example
create mode 100644 templates/nextjs-minimal/README.md
create mode 100644 templates/nextjs-minimal/app/admin/layout.tsx
create mode 100644 templates/nextjs-minimal/app/admin/page.tsx
create mode 100644 templates/nextjs-minimal/app/admin/roles/page.tsx
create mode 100644 templates/nextjs-minimal/app/admin/settings/page.tsx
create mode 100644 templates/nextjs-minimal/app/admin/users/[authId]/page.tsx
create mode 100644 templates/nextjs-minimal/app/admin/users/page.tsx
create mode 100644 templates/nextjs-minimal/app/api/admin/export/route.ts
create mode 100644 templates/nextjs-minimal/app/api/auth/[...all]/route.ts
create mode 100644 templates/nextjs-minimal/app/api/files/register/route.ts
create mode 100644 templates/nextjs-minimal/app/api/files/upload/route.ts
create mode 100644 templates/nextjs-minimal/app/api/files/view/route.ts
create mode 100644 templates/nextjs-minimal/app/api/hackkit/[...path]/route.ts
create mode 100644 templates/nextjs-minimal/app/app-bar.tsx
create mode 100644 templates/nextjs-minimal/app/contact/page.tsx
create mode 100644 templates/nextjs-minimal/app/dashboard/layout.tsx
create mode 100644 templates/nextjs-minimal/app/dashboard/page.tsx
create mode 100644 templates/nextjs-minimal/app/globals.css
create mode 100644 templates/nextjs-minimal/app/help/page.tsx
create mode 100644 templates/nextjs-minimal/app/i/approval/layout.tsx
create mode 100644 templates/nextjs-minimal/app/i/approval/page.tsx
create mode 100644 templates/nextjs-minimal/app/layout.tsx
create mode 100644 templates/nextjs-minimal/app/onboarding/hacker/hacker-form-defaults.ts
create mode 100644 templates/nextjs-minimal/app/onboarding/hacker/hacker-registration-client.tsx
create mode 100644 templates/nextjs-minimal/app/onboarding/hacker/page.tsx
create mode 100644 templates/nextjs-minimal/app/onboarding/hacktag/page.tsx
create mode 100644 templates/nextjs-minimal/app/onboarding/layout.tsx
create mode 100644 templates/nextjs-minimal/app/onboarding/onboarding-shell.tsx
create mode 100644 templates/nextjs-minimal/app/onboarding/user-data/page.tsx
create mode 100644 templates/nextjs-minimal/app/page.tsx
create mode 100644 templates/nextjs-minimal/app/participant-layout.tsx
create mode 100644 templates/nextjs-minimal/app/profile-menu.tsx
create mode 100644 templates/nextjs-minimal/app/providers.tsx
create mode 100644 templates/nextjs-minimal/app/register/page.tsx
create mode 100644 templates/nextjs-minimal/app/registration-closed/page.tsx
create mode 100644 templates/nextjs-minimal/app/settings/layout.tsx
create mode 100644 templates/nextjs-minimal/app/settings/page.tsx
create mode 100644 templates/nextjs-minimal/app/settings/profile-settings-client.tsx
create mode 100644 templates/nextjs-minimal/app/settings/registration/page.tsx
create mode 100644 templates/nextjs-minimal/app/sign-in/auth-form.tsx
create mode 100644 templates/nextjs-minimal/app/sign-in/page.tsx
create mode 100644 templates/nextjs-minimal/app/sign-up/page.tsx
create mode 100644 templates/nextjs-minimal/app/suspended/page.tsx
create mode 100644 templates/nextjs-minimal/app/user/[tag]/page.tsx
create mode 100644 templates/nextjs-minimal/db/migrations/sqlite/.snapshot.json
create mode 100644 templates/nextjs-minimal/db/migrations/sqlite/Migration20261020200349_initial.ts
create mode 100644 templates/nextjs-minimal/env.ts
create mode 100644 templates/nextjs-minimal/hackkit.config.ts
create mode 100644 templates/nextjs-minimal/hackkit.lock
create mode 100644 templates/nextjs-minimal/lib/app-config.ts
create mode 100644 templates/nextjs-minimal/lib/auth-client.ts
create mode 100644 templates/nextjs-minimal/lib/auth-options.ts
create mode 100644 templates/nextjs-minimal/lib/auth.ts
create mode 100644 templates/nextjs-minimal/lib/blob.ts
create mode 100644 templates/nextjs-minimal/lib/database-config.ts
create mode 100644 templates/nextjs-minimal/lib/hackkit-ui-routes.ts
create mode 100644 templates/nextjs-minimal/lib/logger.ts
create mode 100644 templates/nextjs-minimal/lib/onboarding.ts
create mode 100644 templates/nextjs-minimal/lib/public-site-config.ts
create mode 100644 templates/nextjs-minimal/lib/runtime.ts
create mode 100644 templates/nextjs-minimal/lib/upload-profile-photo.ts
create mode 100644 templates/nextjs-minimal/lib/upload-resume.ts
create mode 100644 templates/nextjs-minimal/middleware.ts
create mode 100644 templates/nextjs-minimal/next-env.d.ts
create mode 100644 templates/nextjs-minimal/next.config.mjs
create mode 100644 templates/nextjs-minimal/package.json
create mode 100644 templates/nextjs-minimal/postcss.config.cjs
create mode 100644 templates/nextjs-minimal/scripts/database.ts
create mode 100644 templates/nextjs-minimal/scripts/db.mjs
rename {apps/web => templates/nextjs-minimal}/tailwind.config.ts (100%)
create mode 100644 templates/nextjs-minimal/tsconfig.json
diff --git a/.github/workflows/apps.yml b/.github/workflows/apps.yml
new file mode 100644
index 000000000..2923951a7
--- /dev/null
+++ b/.github/workflows/apps.yml
@@ -0,0 +1,82 @@
+name: Demo and documentation
+
+on:
+ pull_request:
+ paths:
+ - "apps/demo/**"
+ - "apps/docs/**"
+ - "packages/**"
+ - "package.json"
+ - "pnpm-workspace.yaml"
+ - ".npmrc"
+ - "pnpm-lock.yaml"
+ - "turbo.json"
+ - "tooling/**"
+ - ".github/workflows/apps.yml"
+
+jobs:
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm build:docs
+ - run: pnpm --filter @hackkit/docs typecheck
+ - run: pnpm --filter @hackkit/docs deploy:check
+ demo:
+ runs-on: ubuntu-latest
+ services:
+ libsql:
+ image: ghcr.io/tursodatabase/libsql-server:latest
+ env:
+ SQLD_NODE: standalone
+ SQLD_HTTP_LISTEN_ADDR: 0.0.0.0:8080
+ ports:
+ - 8080:8080
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm exec turbo run build --filter=@hackkit/demo^...
+ - run: pnpm --filter @hackkit/demo plugin:sync
+ - run: git diff --exit-code -- apps/demo/app apps/demo/hackkit.lock
+ - run: pnpm --filter @hackkit/demo typecheck
+ - run: node tooling/check-client-bundle.mjs
+ - name: Configure disposable database and local Worker bindings
+ run: |
+ mkdir -p apps/demo/.wrangler
+ cat > apps/demo/.dev.vars <<'EOF'
+ TURSO_DATABASE_URL=http://127.0.0.1:8080
+ TURSO_AUTH_TOKEN=local-ci-no-remote-access
+ BETTER_AUTH_SECRET=ci-only-secret-for-disposable-demo-testing
+ BETTER_AUTH_URL=http://localhost:3001
+ EOF
+ curl --fail --retry 20 --retry-connrefused --retry-delay 2 http://127.0.0.1:8080/health
+ - run: pnpm --filter @hackkit/demo db:demo
+ - run: pnpm --filter @hackkit/demo db:check
+ - run: pnpm --filter @hackkit/demo test:database
+ - run: pnpm --filter @hackkit/demo build
+ - run: pnpm --filter @hackkit/demo deploy:check
+ - run: pnpm --filter @hackkit/demo exec playwright install --with-deps chromium
+ - run: pnpm --filter @hackkit/demo test:smoke
+ env:
+ DEMO_TEST_URL: http://localhost:3001
+ - name: Upload failure diagnostics
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: demo-diagnostics
+ path: |
+ apps/demo/test-results
+ apps/demo/playwright-report
+ apps/demo/.wrangler/ci-server.log
+ if-no-files-found: ignore
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
new file mode 100644
index 000000000..df2c3d9a7
--- /dev/null
+++ b/.github/workflows/integration.yml
@@ -0,0 +1,96 @@
+name: Templates and E2E
+
+on:
+ pull_request:
+ paths:
+ - "templates/**"
+ - "e2e/**"
+ - "packages/**"
+ - "tooling/**"
+ - "package.json"
+ - "pnpm-workspace.yaml"
+ - ".npmrc"
+ - "pnpm-lock.yaml"
+ - "turbo.json"
+ - ".github/workflows/integration.yml"
+
+jobs:
+ packages:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "22.17.0"
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm test
+ - run: pnpm exec turbo run typecheck --filter='./packages/*'
+
+ e2e:
+ runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres:16
+ env:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: hackkit
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd "pg_isready -U postgres -d hackkit"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ mysql:
+ image: mysql:8.4
+ env:
+ MYSQL_ROOT_PASSWORD: mysql
+ MYSQL_DATABASE: hackkit
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd "mysqladmin ping -h 127.0.0.1 -pmysql"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 10
+ libsql:
+ image: ghcr.io/tursodatabase/libsql-server:latest
+ env:
+ SQLD_NODE: standalone
+ SQLD_HTTP_LISTEN_ADDR: 0.0.0.0:8080
+ ports:
+ - 8080:8080
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "22.17.0"
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm --filter @hackkit/e2e exec playwright install --with-deps chromium
+ - name: Wait for network libSQL
+ run: curl --fail --retry 20 --retry-connrefused --retry-delay 2 http://127.0.0.1:8080/health
+ - name: Validate templates and all integrations
+ env:
+ HACKKIT_REQUIRE_DATABASE_MATRIX: "1"
+ HACKKIT_TEST_POSTGRESQL_URL: postgresql://postgres:postgres@127.0.0.1:5432/hackkit
+ HACKKIT_TEST_MYSQL_URL: mysql://root:mysql@127.0.0.1:3306/hackkit
+ HACKKIT_TEST_LIBSQL_URL: http://127.0.0.1:8080
+ run: pnpm test:e2e
+ - name: Verify generated plugin files are committed
+ run: git diff --exit-code -- templates/*/app templates/*/hackkit.lock
+ - name: Upload failure diagnostics
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-diagnostics
+ path: |
+ e2e/test-results
+ e2e/playwright-report
+ if-no-files-found: ignore
diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
deleted file mode 100644
index e2a0dc4be..000000000
--- a/.github/workflows/web.yml
+++ /dev/null
@@ -1,132 +0,0 @@
-name: Web
-
-on:
- pull_request:
- paths:
- - "apps/web/**"
- - "packages/**"
- - "pnpm-lock.yaml"
- - "turbo.json"
- - ".github/workflows/web.yml"
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- services:
- postgres:
- image: postgres:16
- env:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: postgres
- POSTGRES_DB: hackkit
- ports:
- - 5432:5432
- options: >-
- --health-cmd "pg_isready -U postgres -d hackkit"
- --health-interval 10s
- --health-timeout 5s
- --health-retries 5
-
- mysql:
- image: mysql:8.4
- env:
- MYSQL_ROOT_PASSWORD: mysql
- MYSQL_DATABASE: hackkit
- ports:
- - 3306:3306
- options: >-
- --health-cmd "mysqladmin ping -h 127.0.0.1 -pmysql"
- --health-interval 10s
- --health-timeout 5s
- --health-retries 10
- libsql:
- image: ghcr.io/tursodatabase/libsql-server:latest
- env:
- SQLD_NODE: standalone
- SQLD_HTTP_LISTEN_ADDR: 0.0.0.0:8080
- ports:
- - 8080:8080
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up pnpm
- uses: pnpm/action-setup@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "22.17.0"
- cache: pnpm
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
-
- - name: Build web dependency graph
- run: pnpm exec turbo run build --filter=web^...
-
- - name: Sync plugin files
- working-directory: apps/web
- run: pnpm plugin:sync
-
- - name: Verify generated plugin files are current
- run: git diff --exit-code -- apps/web/app apps/web/hackkit.lock
-
- - name: Wait for network libSQL
- run: curl --fail --retry 20 --retry-connrefused --retry-delay 2 http://127.0.0.1:8080/health
-
- - name: Test all SQL drivers against committed migrations
- env:
- HACKKIT_REQUIRE_DATABASE_MATRIX: "1"
- HACKKIT_TEST_POSTGRESQL_URL: postgresql://postgres:postgres@127.0.0.1:5432/hackkit
- HACKKIT_TEST_MYSQL_URL: mysql://root:mysql@127.0.0.1:3306/hackkit
- HACKKIT_TEST_LIBSQL_URL: http://127.0.0.1:8080
- run: pnpm --filter web test:db-matrix
-
- - name: Test database workflow isolation
- run: pnpm --filter web test:db-workflow
-
- - name: Package tests
- run: |
- pnpm --filter web test
- pnpm --filter @hackkit/core test
- pnpm --filter @hackkit/cli test
- pnpm --filter @hackkit/config test
- pnpm --filter @hackkit/next test
- pnpm --filter @hackkit/plugin-teams test
- pnpm --filter @hackkit/plugin-discord test
- pnpm --filter @hackkit/plugin-notifications-email test
-
- - name: Prepare isolated build database
- env:
- DATABASE_URL: file:.data/ci.db
- run: |
- mkdir -p apps/web/.data
- pnpm --filter web db:migrate
- pnpm --filter web db:seed
-
- - name: Typecheck
- run: pnpm --filter web typecheck
-
- - name: Build web
- env:
- DATABASE_URL: file:.data/ci.db
- NEXT_PUBLIC_APP_URL: https://web-ci.example.com
- BETTER_AUTH_URL: https://web-ci.example.com
- BETTER_AUTH_SECRET: ci-web-secret-that-is-at-least-32-characters
- HACKKIT_BLOB_ADAPTER: s3
- HACKKIT_S3_BUCKET: ci-bucket
- HACKKIT_S3_REGION: auto
- HACKKIT_S3_ENDPOINT: https://example.r2.cloudflarestorage.com
- HACKKIT_S3_ACCESS_KEY_ID: ci-access-key
- HACKKIT_S3_SECRET_ACCESS_KEY: ci-secret-key
- DISCORD_GUILD_ID: ci-guild
- DISCORD_BOT_API_URL: https://discord-bot-ci.example.com
- DISCORD_INTERNAL_AUTH_KEY: ci-internal-auth-key
- DISCORD_PARTICIPANT_ROLE_ID: ci-participant-role
- run: pnpm --filter web build
-
- - name: Test production HTTP runtime
- run: pnpm --filter web test:runtime
diff --git a/.gitignore b/.gitignore
index 84cb2b5d5..de38cdb1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,7 +47,7 @@ yarn-error.log*
sessions/
# web local sqlite
-apps/web/.data/
+templates/*/.data/
# cursor
.cursor/plans/
@@ -57,5 +57,33 @@ HANDOFF.md
# build
**/dist/
+experiments/
+scripts/
+
# Local research notes
/docs/research/**/*.md
+
+# Template-owned database commands must be tracked.
+!templates/*/scripts/
+!templates/*/scripts/**
+
+# E2E artifacts
+e2e/playwright-report/
+e2e/test-results/
+
+# Deployable applications
+apps/*/.data/
+.wrangler/
+.dev.vars
+.dev.vars.*
+!apps/demo/scripts/
+!apps/demo/scripts/**
+
+apps/demo/playwright-report/
+apps/demo/test-results/
+
+# A credential-free reference for the demo Worker.
+!apps/demo/.dev.vars.example
+
+.vinext/
+apps/demo/lib/generated/
diff --git a/.npmrc b/.npmrc
index ded82e2f6..b257f8ad4 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1 +1,4 @@
auto-install-peers = true
+# Keep the React 18 templates and React 19 Worker/docs dependency contexts separate.
+resolve-peers-from-workspace-root = false
+dedupe-peer-dependents = false
diff --git a/.prettierignore b/.prettierignore
index b29406434..b6f949648 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,4 +2,16 @@ pnpm-lock.yaml
pnpm-workspace.yaml
**/package.json
*.json
-apps/web/db/migrations/**
+templates/*/db/migrations/**
+
+e2e/playwright-report/
+e2e/test-results/
+
+apps/docs/.source/
+apps/docs/out/
+apps/demo/playwright-report/
+apps/demo/test-results/
+
+apps/demo/lib/generated/
+apps/demo/db/migrations/
+apps/demo/worker/env.d.ts
diff --git a/.prettierrc b/.prettierrc
index 836c001e9..a3051e7f3 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,6 +1,6 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
- "tailwindConfig": "./apps/web/tailwind.config.ts",
+ "tailwindConfig": "./templates/nextjs-full/tailwind.config.ts",
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
diff --git a/.tours/01-repository-map.tour b/.tours/01-repository-map.tour
new file mode 100644
index 000000000..799588472
--- /dev/null
+++ b/.tours/01-repository-map.tour
@@ -0,0 +1,72 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "01. Repository map and composition",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Start here with the repository root open in VS Code. These linked chapters explain the `templates-and-testing` working tree, including the template/E2E split. They use current files rather than a pinned commit, because the split includes uncommitted changes.\n\nFollow a participant who signs up, completes registration, creates a team, and receives event access. You will trace configuration into Core, then follow browser requests back to domain operations and persistence. Later chapters cover route generation, integrations, deployed apps, and verification.\n\nPrerequisites: TypeScript and basic React knowledge. An EntityManager is MikroORM's object for querying and tracking database records. A scope is one execution's EntityManager plus APIs that use it. Reading these tours does not start services, run migrations, or send external requests."
+ },
+ {
+ "file": "pnpm-workspace.yaml",
+ "pattern": "packages:",
+ "title": "Locate each kind of workspace",
+ "description": "The workspace includes `packages/*`, `templates/*`, `apps/*`, and `e2e`. Packages contain reusable behavior. Templates are runnable reference applications. Apps contain the demo and documentation sites. E2E owns integration checks that exercise the real templates.\n\nThe full and minimal templates are application consumers of the packages. E2E does not keep a third copy of the application. Read this map before searching for the former `apps/web` path."
+ },
+ {
+ "file": "package.json",
+ "pattern": "\"dev\": \"turbo run dev --filter=@hackkit/template-nextjs-full\"",
+ "title": "Choose the application explicitly",
+ "description": "Root `dev`, `start`, and `db:*` commands target the full template. `dev:minimal` selects the Core-only starter. `test` selects package tests, while `test:e2e` selects the integration workspace. Demo and docs have their own commands.\n\nUse the pinned pnpm through Corepack as described in the root README. These are command definitions to inspect, not instructions to reset or migrate a developer database. The database chapter explains their effects."
+ },
+ {
+ "file": "turbo.json",
+ "pattern": "\"tasks\": \\{",
+ "title": "Read the task graph",
+ "description": "The `build` task depends on `^build`, which means the build task of dependency packages. A template can therefore import a library's `dist` output after Turbo builds that library. This is different from asking a package to depend on its own build.\n\nDevelopment and start tasks are persistent and uncached. Typechecking also builds dependencies first. The demo has a separate build override, and E2E tests disable caching. Outputs describe which generated files Turbo can restore; they do not replace runtime validation."
+ },
+ {
+ "file": "packages/tsconfig/base.json",
+ "pattern": "\"strict\": true",
+ "title": "Share compiler defaults without hiding package boundaries",
+ "description": "The tsconfig workspace provides shared base, React-library, and Next configurations. Strict checking is a shared baseline. Individual packages still choose their own includes, outputs, and appropriate compiler configuration.\n\nCore's tsup.config.ts declares separate server, browser, testing, HTTP, and event-pass build entries and produces declarations. Package exports determine which entry a consumer imports. A source-level separation must also survive the build and published export map."
+ },
+ {
+ "file": ".npmrc",
+ "pattern": "resolve-peers-from-workspace-root = false",
+ "title": "Keep the React dependency contexts separate",
+ "description": "The reference templates use React 18, while the Worker/demo and docs applications use React 19. These pnpm settings keep peer resolution from automatically using the workspace root and disable deduplication across peer contexts.\n\nA workspace install can therefore include different React contexts for separate applications. Inspect each package's actual dependencies before changing this setting or assuming all consumers run one framework version. The lockfile records the resolved contexts."
+ },
+ {
+ "file": "packages/core/package.json",
+ "pattern": "\"\\./client\": \\{",
+ "title": "Find the browser and server entry points",
+ "description": "Core publishes several entry points. The main entry contains server/domain behavior. `/client` exports browser-safe schemas, constants, and DTO types. `/http` contains server endpoint construction. `/http/client` contains the browser transport.\n\nA DTO is a plain data object returned across a boundary. Importing its TypeScript type does not require importing the server implementation at runtime. This separation lets shared UI use validation and inferred endpoint types without bundling MikroORM or database drivers."
+ },
+ {
+ "file": "templates/nextjs-full/hackkit.config.ts",
+ "pattern": "export default defineHackkitConfig\\(\\{",
+ "title": "Compose the full application",
+ "description": "The application chooses database options, Better Auth options, seed roles, file storage, registration choices, groups, event types, and plugin instances. Teams, Discord, and email notifications are declared in the `plugins` array below.\n\nThis file is the composition point. Core supplies behavior, but the host chooses which services and plugins exist. The plugin objects will feed both ORM discovery and scope creation. Provider factories defer optional service configuration until a plugin API is set up."
+ },
+ {
+ "file": "templates/nextjs-minimal/hackkit.config.ts",
+ "pattern": "export default defineHackkitConfig\\(\\{",
+ "title": "Compare the minimal consumer",
+ "description": "The minimal template demonstrates Core registration, profiles, and administration without the full template's plugin set. Read its actual `plugins`, database, blob, and role configuration beside the full template.\n\nCore and Better Auth still need tables and migrations even when no feature plugins are enabled. The minimal template therefore has its own committed SQLite migration. Removing optional plugins does not remove the Core/auth lifecycle."
+ },
+ {
+ "file": "packages/config/src/index.ts",
+ "pattern": "export function defineHackkitConfig",
+ "title": "Preserve the caller's inferred config type",
+ "description": "`defineHackkitConfig` returns its input unchanged. The const generic preserves specific configuration information in the inferred return type. This matters for literal plugin IDs and the API types associated with those plugins.\n\nIt does not validate environment credentials or initialize resources. `resolveHackkitConfig` below verifies that database driver and auth options exist, then supplies empty plugin and role lists when omitted. `loadHackkitConfig` uses Jiti to import a TypeScript config for CLI callers."
+ },
+ {
+ "file": "templates/nextjs-full/lib/runtime.ts",
+ "pattern": "const host = createHackkitRuntimeFromConfig\\(\\{",
+ "title": "Connect application configuration to execution",
+ "description": "This server-only module creates the Next integration host from normalized app configuration. `setHackkitRuntime(host.getRuntime)` registers a factory for plugin-owned server pages. Convenience functions expose the current user's runtime and domain APIs.\n\nThe host keeps shared initialization, while `getRuntime` supplies request/render-specific state. The exported handler serves the catch-all HTTP route. Continue with [02. Core startup and persistence] to see what the host initializes."
+ }
+ ],
+ "isPrimary": true,
+ "nextTour": "02. Core startup and persistence"
+}
diff --git a/.tours/02-core-startup.tour b/.tours/02-core-startup.tour
new file mode 100644
index 000000000..b63899bb9
--- /dev/null
+++ b/.tours/02-core-startup.tour
@@ -0,0 +1,83 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "02. Core startup and persistence",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Goal: explain which resources are shared and which are rebuilt for each execution. Trace configuration into the entity registry, ORM, Better Auth, and a scoped `hackkit` object. Then inspect migration and role-seeding ownership.\n\nA database connection can be shared through an ORM instance. Managed entity objects and the current actor must belong to the execution that uses them."
+ },
+ {
+ "file": "packages/core/src/runtime.ts",
+ "pattern": "export async function initializeHackkit",
+ "title": "Initialize shared resources once",
+ "description": "`initializeHackkit` receives database/auth options, plugins, optional app entities, and domain configuration. It builds a registry, initializes MikroORM using `createOrmOptions`, and creates Better Auth with the same ORM.\n\nIf auth construction fails after ORM initialization, it closes the ORM before rethrowing. The returned runtime exposes the shared `orm`, `auth`, and `registry`, plus factories for isolated executions. Startup does not apply migrations or seed roles."
+ },
+ {
+ "file": "packages/core/src/plugins.ts",
+ "pattern": "export function createPluginRegistry",
+ "title": "Discover the complete schema",
+ "description": "The registry begins with Better Auth entities, Core models, Core permissions, and Core settings. Each plugin contributes its declarations, and application entities are appended afterward.\n\nThe function rejects duplicate plugin IDs, invalid plugin namespaces, duplicate setting/permission values, and duplicate entity names or tables. These checks run before the ORM relies on the schema. The plugin chapter will examine the runtime API contract separately."
+ },
+ {
+ "file": "packages/core/src/mikro/options.ts",
+ "pattern": "export function createOrmOptions",
+ "title": "Use the same entity collection for runtime and migrations",
+ "description": "`createOrmOptions` rebuilds the registry from config, verifies that auth's requested tables and fields exist, and returns native MikroORM options with that entity list. It adds the Migrator extension while retaining configured extensions.\n\nBoth runtime initialization and database tools call this function. A plugin entity therefore enters ORM metadata through the same path in both places. Metadata discovery describes the desired schema; committed migrations still control changes to an existing database."
+ },
+ {
+ "file": "packages/core/src/models.ts",
+ "pattern": "export const HackKitUser = defineEntity\\(\\{",
+ "title": "Keep one canonical user record",
+ "description": "`HackKitUser` maps to `core_user`. It contains auth identity fields and domain fields such as HackTag, role ID, approval status, and check-in time. The role reference maps to a primary key and uses a nullable foreign key.\n\nThe `p` builders define native MikroORM properties, defaults, relationships, and indexes. Nearby Core entities represent participant data, hacker registration, events, RSVP, settings, notifications, and operation locks. Plain domain types describe returned values; entity definitions describe persistence."
+ },
+ {
+ "file": "packages/core/src/mikro/auth.ts",
+ "pattern": "export function resolveHackKitAuthOptions",
+ "title": "Map Better Auth onto Core's user table",
+ "description": "The application provides auth settings, but Core forces the user model to `core_user` and maps Better Auth's `image` field to `profilePhotoUrl`. Core creates the MikroORM database adapter in `createHackKitAuth`.\n\nThis avoids a second application profile row that would need synchronization with the auth user. Credentials, session policy, and providers still come from host options. Domain privileges stay behind Core APIs."
+ },
+ {
+ "file": "packages/core/src/mikro/auth.ts",
+ "pattern": "export function assertAuthEntitySchema",
+ "title": "Reject unsupported auth schema before use",
+ "description": "Better Auth can require tables and fields according to its configuration. This function compares those requirements with registered entity metadata. A missing model or property throws with a request for a native entity and reviewed migration.\n\nIt also rejects exposing Core-owned user properties through auth field configuration. Enabling an auth feature is therefore a schema decision, not just a new provider option."
+ },
+ {
+ "file": "packages/core/src/runtime.ts",
+ "pattern": "function createScope\\(actorAuthId\\?: string\\)",
+ "title": "Fork the EntityManager and construct scoped APIs",
+ "description": "Each call forks `orm.em` with context support and calls `createHackkit` using the fork, the shared registry, and the optional actor ID. A fork has its own identity map, the set of managed entity instances used by that execution.\n\nThe resulting scope contains `em` and `hackkit`. Plugin setup receives this same EntityManager. Do not retain this scoped API object as a global singleton. A later request must create its own scope."
+ },
+ {
+ "file": "packages/core/src/runtime.ts",
+ "pattern": "return RequestContext\\.create\\(scope\\.em, \\(\\) => operation\\(scope\\)\\);",
+ "title": "Run a non-render operation in an ORM context",
+ "description": "`run` creates a scope, then executes the callback inside MikroORM's RequestContext. The callback receives the scope and returns a promise whose result becomes the result of `run`.\n\nThe HTTP handler uses this path after resolving a session. Context-aware ORM operations can resolve the correct manager during async work. Creating a request context is not a transaction around the whole request; domain operations create transactions where required."
+ },
+ {
+ "file": "packages/core/src/hackkit.ts",
+ "pattern": "export function createHackkit",
+ "title": "Assemble Core around explicit dependencies",
+ "description": "`createHackkit` accepts an existing EntityManager. It resolves clock, ID generator, registration choices, event types, groups, and logger. It creates access control, settings, notifications, plugin APIs, and the domain API groups.\n\nThe function returns methods, not a listening server. Tests can inject time and ID functions. HTTP and Next integrations decide how to obtain the actor and invoke these methods. Keep transport details out of this composition function."
+ },
+ {
+ "file": "templates/nextjs-full/scripts/database.ts",
+ "pattern": "export async function runDatabaseCommand",
+ "title": "Keep schema changes explicit and application-owned",
+ "description": "The template's database command runner owns generate, migrate, rollback, status, check, seed, and reset operations. It uses `createOrmOptions`, so it sees Core/auth/plugin/app entities, and always closes the ORM in `finally`.\n\n`check` examines pending migration history, entity-versus-snapshot differences, and live schema SQL. `reset` is destructive and restricted to a local SQLite database in development or tests. Reading this tour does not invoke it. Migration directories remain in the template that owns the deployed database."
+ },
+ {
+ "file": "packages/core/src/seed.ts",
+ "pattern": "export async function seedRoles",
+ "title": "Seed roles without overwriting existing policy",
+ "description": "`seedRoles` inserts configured role IDs that are missing and skips IDs already present. It returns inserted and skipped IDs. Re-running it does not overwrite an administrator's existing role permissions.\n\nInitialization does not assign owner privileges automatically. Configured seed roles and the host's owner bootstrap procedure are separate concerns. Follow the template README before administering a real instance."
+ },
+ {
+ "file": "packages/core/src/runtime.test.ts",
+ "pattern": "it\\(\"shares auth and connections while isolating domain execution scopes\"",
+ "title": "Check the shared-versus-scoped contract",
+ "description": "This test inspects runtime behavior across scopes. Read the assertions for different EntityManagers and domain reads, rather than assuming that shared auth implies shared managed entities.\n\nAlso inspect [auth lifecycle tests](./packages/core/src/mikro/auth-lifecycle.test.ts) for canonical user updates and privileged-field handling. These are source references, not a claim that the tests ran while authoring this tour. Continue with [03. Core domain behavior]."
+ }
+ ],
+ "nextTour": "03. Core domain behavior"
+}
diff --git a/.tours/03-core-domain.tour b/.tours/03-core-domain.tour
new file mode 100644
index 000000000..924b4b92f
--- /dev/null
+++ b/.tours/03-core-domain.tour
@@ -0,0 +1,119 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "03. Core domain behavior",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Goal: trace the participant lifecycle through Core's actual functions. The scenario is a signed-in user who claims a HackTag, completes personal data, registers as a hacker, receives approval if required, and confirms attendance.\n\nServer callers must supply a trusted actor or target as each method requires. Authentication, authorization, validation, and transactions solve different problems. Watch where each one occurs."
+ },
+ {
+ "file": "packages/core/src/hackkit.ts",
+ "pattern": "const runtimeContext: HackkitRuntimeContext = \\{",
+ "title": "Pass only the dependencies each API needs",
+ "description": "The context collects persistence, time, IDs, logging, configuration, permission helpers, and notifications. API factories receive this context or a typed subset of it.\n\n`createUsersApi` also receives registration policy and group behavior. `createEventsApi`, `createRsvpApi`, and `createAdminApi` return domain methods. These closures keep shared rules available without making every operation import a global runtime."
+ },
+ {
+ "file": "packages/core/src/errors.ts",
+ "pattern": "export function parseInput",
+ "title": "Validate unknown domain inputs",
+ "description": "Core accepts `unknown` at many mutation boundaries. `parseInput` checks a Zod schema and turns validation failure into HackKit's domain error format. Successful parsing supplies the typed value used by the operation.\n\nHTTP endpoints also validate browser bodies, but Core methods remain usable from scripts and server code. Domain checks still need to run regardless of transport. Examine `schemas.ts` for field constraints and `user-data-options.ts` for configurable choices."
+ },
+ {
+ "file": "packages/core/src/domain-log.ts",
+ "pattern": "export async function withDomainLog",
+ "title": "Log domain outcomes without changing returned errors",
+ "description": "This wrapper runs a domain callback and logs success when it returns. A known HackKitError records the stable error code and message. Unexpected errors use the error log level. Every failure is rethrown for the caller or HTTP layer to handle.\n\nThe logger is injected through Core configuration. This helper observes a callback outcome, not the final commit of every possible surrounding transaction. It is not a durable transaction audit table."
+ },
+ {
+ "file": "packages/core/src/functions/users.ts",
+ "pattern": "async claimHackTag\\(input: unknown\\)",
+ "title": "Claim a public identifier",
+ "description": "`claimHackTag` parses the actor's request, uses an operation lock, and checks whether the requested HackTag is available before updating the canonical user. Follow normalization and conflict handling in the function.\n\nThis is the first participant-specific identity step after auth signup. The auth ID remains the persistence identity; the HackTag is a user-facing lookup value. The browser endpoint must supply the actor from its session."
+ },
+ {
+ "file": "packages/core/src/hackkit.ts",
+ "pattern": "async completeUserData\\(input: unknown\\)",
+ "title": "Persist personal data before hacker registration",
+ "description": "The configured complete-user-data schema validates the input. Core checks the user exists, then updates or creates the user's data record inside the `user-data` operation lock. It preserves the original completion timestamp on edits and updates the modification timestamp.\n\nA completed auth account alone does not imply this row exists. `registerHacker` checks it explicitly. This ordering is also visible in the page guards and onboarding UI."
+ },
+ {
+ "file": "packages/core/src/hackkit.ts",
+ "pattern": "async function registerHacker\\(input: unknown\\)",
+ "title": "Register atomically and preserve edit behavior",
+ "description": "`registerHacker` locks the registration decision, parses the input, and requires both an existing user and completed personal data. An existing hacker record is updated without re-running the policy for a new admission.\n\nFor a new record, the registration policy checks availability. Core creates the hacker, looks up the configured competitor role if present, and updates the user's role/approval fields before flushing. Failure must roll back the whole operation, including the hacker insert. The method returns a serialized Hacker value."
+ },
+ {
+ "file": "packages/core/src/functions/registration-policy.ts",
+ "pattern": "async assertCanRegisterNewHacker\\(\\)",
+ "title": "Separate registration count from approved capacity",
+ "description": "The policy checks whether registration is open and whether the maximum registration count has been reached. A positive limit bounds admission; zero means unlimited.\n\nWhen approval is required, a new registration can remain pending. When approval is automatic, it also checks approved-hacker capacity before admission. `assertCanApproveUser` applies that capacity check when an existing registered user is approved later. Registration capacity and RSVP capacity are separate settings."
+ },
+ {
+ "file": "packages/core/src/functions/users.ts",
+ "pattern": "async approveUser\\(input: unknown\\)",
+ "title": "Approve within capacity and assign a participant group",
+ "description": "`approveUser` shares the registration lock with admission. It requires `UsersApprove`, checks capacity when changing an unapproved user to approved, and applies role hierarchy to the target. It writes approval and then calls `groups.assignNextGroup` when approval is requested.\n\nThe group helper joins the surrounding transaction and keeps an existing assignment. This explicit approval path connects registration policy to group allocation. Automatic approval during new hacker registration does not call this function, so do not assume those paths perform identical group work."
+ },
+ {
+ "file": "packages/core/src/functions/groups.ts",
+ "pattern": "async assignNextGroup\\(authId: AuthId\\)",
+ "title": "Choose the least-populated enabled group",
+ "description": "The method locks group allocation, returns null for a missing hacker, and preserves any existing group. With enabled groups, it counts registered hackers assigned to each, sorts by count, and breaks ties using configuration order.\n\nIt writes the chosen group to the hacker record and updates its timestamp. The result is a Hacker DTO. This module chooses an application group; Discord later translates that group into its configured role plan."
+ },
+ {
+ "file": "packages/core/src/mikro/operation.ts",
+ "pattern": "export async function withOperationLock",
+ "title": "Protect read/check/write decisions across connections",
+ "description": "`withOperationLock` opens or joins a required transaction. It upserts a lock row with a fresh token and flushes before running the callback. The write obtains a database write/row lock before checks read capacity or membership.\n\nRelated operations use the same key, such as `registration` or `teams.membership`. A JavaScript mutex would protect only one process. This lock uses the database so separate connections coordinate. Unique constraint errors become `CONFLICT`. If this helper owns the transaction, it clears managed state after completion or failure. External network effects belong after commit."
+ },
+ {
+ "file": "packages/core/src/access-control.ts",
+ "pattern": "async requirePermission\\(",
+ "title": "Load the principal and enforce role permissions",
+ "description": "An access principal contains the current user and their role. `requirePermission` resolves both and rejects a missing role or permission. `hasPermission` returns a boolean for a user with no role rather than throwing for that case.\n\nRole hierarchy is another rule. Lower position numbers outrank higher numbers, while SuperAdmin bypasses hierarchy. `assertCanGrantPermissions` prevents a non-super-admin from granting the SuperAdmin permission. A signed-in session alone does not grant administration rights."
+ },
+ {
+ "file": "packages/core/src/hackkit.ts",
+ "pattern": "async assignRoleToUser\\(input: unknown\\)",
+ "title": "Check both current and replacement roles",
+ "description": "Role assignment requires `RolesAssign`. The actor must be allowed to manage the proposed role and, if the user has a current role, that current role too. The update occurs inside the roles operation lock.\n\nThis prevents an actor from bypassing hierarchy by demoting or replacing a role they cannot manage. Nearby create/update/delete methods apply their own permission, hierarchy, and assignment checks. Use these methods rather than writing `roleId` from a browser body."
+ },
+ {
+ "file": "packages/core/src/functions/settings.ts",
+ "pattern": "async function getValue\\(key: SettingKey\\)",
+ "title": "Resolve stored values against registered definitions",
+ "description": "Settings definitions come from Core and plugins. Reads resolve the registered definition and stored override; absent overrides use the definition's default. Mutation methods below require settings-management permission and validate values against each definition.\n\nThis is how a plugin reads a team-size limit without owning a separate settings subsystem. Changing an app config default and writing a persisted setting are different operations. Inspect the resolver above this function to see which value wins."
+ },
+ {
+ "file": "packages/core/src/functions/rsvp.ts",
+ "pattern": "async confirm\\(input: unknown\\)",
+ "title": "Confirm attendance or allocate a waitlist place",
+ "description": "`confirm` uses the `rsvp` lock, requires an approved registered hacker, and checks that RSVPs are open. An existing non-cancelled RSVP is returned unchanged. Otherwise it counts active confirmed records and chooses confirmed, waitlisted, or an error according to settings.\n\nThe function writes the RSVP and queues a notification intent before the transaction completes. Cancellation and administrative promotion have their own methods later in the file. Registration approval does not by itself reserve an RSVP place."
+ },
+ {
+ "file": "packages/core/src/functions/users.ts",
+ "pattern": "async checkIn\\(input: unknown\\)",
+ "title": "Record organizer check-in without duplicating arrival",
+ "description": "`checkIn` requires `UsersCheckIn`, loads the target, rejects a user who is already checked in, and writes `checkedInAt` inside the `check-in` operation lock. It returns the refreshed plain user value.\n\nThis function records arrival. Group assignment occurs in the explicit approval path, which calls the groups API. Page guards, approval, check-in, and group assignment are distinct transitions; one timestamp does not establish that all of them occurred."
+ },
+ {
+ "file": "packages/core/src/functions/events.ts",
+ "pattern": "async recordEventScan\\(input: unknown\\)",
+ "title": "Record event participation with prior-scan context",
+ "description": "The event scan operation parses its input, requires scanner permission, checks the event and target user, reads previous scans, and inserts another scan. Its result contains the new scan, prior scans, and a boolean indicating whether prior scans exist.\n\nA scan history can therefore support a repeated-scan warning without silently discarding the new record. Schedule visibility and event administration use separate methods earlier in this file. Inspect those permission checks when adding a new organizer workflow."
+ },
+ {
+ "file": "packages/core/src/functions/admin.ts",
+ "pattern": "async function getOverview\\(input:",
+ "title": "Build administration views from Core records",
+ "description": "The admin API composes permission-checked data into dashboard and user views. It also exposes role/user lookup and export-related behavior. These methods are the server-side aggregation layer used by admin UI, rather than a second source of persistence models.\n\nFor a new dashboard statistic, trace its source records and authorization here before adding the rendered card. This keeps the meaning of a statistic consistent between views."
+ },
+ {
+ "file": "packages/core/src/mikro/domain-transactions.test.ts",
+ "pattern": "it\\(\"admits only one concurrent registration into the final place\"",
+ "title": "Inspect the concurrency and rollback evidence",
+ "description": "This test competes for the last registration place. The earlier test in this file forces a role-assignment failure and checks rollback. Together they target observable domain outcomes, including behavior across transaction boundaries.\n\nFor related rules, read [RSVP tests](./packages/core/src/__tests__/rsvp.test.ts), [check-in tests](./packages/core/src/__tests__/check-in.test.ts), and [group tests](./packages/core/src/__tests__/groups.test.ts). Continue with [04. Plugin contracts and Teams]."
+ }
+ ],
+ "nextTour": "04. Plugin contracts and Teams"
+}
diff --git a/.tours/04-plugin-system.tour b/.tours/04-plugin-system.tour
new file mode 100644
index 000000000..2c2d95c9c
--- /dev/null
+++ b/.tours/04-plugin-system.tour
@@ -0,0 +1,101 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "04. Plugin contracts and Teams",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Goal: understand how to build a plugin that participates in schema discovery, scoped domain APIs, HTTP transport, server pages, and client UI. Teams is the worked example.\n\nThere are two separate installation paths. Core composes runtime declarations and behavior. The CLI copies route reexports into a host application's filesystem. The next chapter explains that filesystem path."
+ },
+ {
+ "file": "packages/core/src/plugins.ts",
+ "pattern": "export type HackKitPlugin<",
+ "title": "Read the plugin contract",
+ "description": "A plugin has a literal `id` and optional `enabled`, `packageName`, `entities`, `permissions`, `settings`, `endpoints`, and `setup` fields. These declarations have different lifetimes.\n\nEntities/settings/permissions describe composition. `endpoints` builds transport definitions around a request runner. `setup` receives an execution context and returns the plugin API. `packageName` lets the CLI find package-owned pages. A plugin may provide only some of these capabilities, as email notifications demonstrates."
+ },
+ {
+ "file": "packages/core/src/plugins.ts",
+ "pattern": "export type HackKitPluginContext = \\{",
+ "title": "Use the execution context instead of global state",
+ "description": "The context supplies the scoped EntityManager, optional actor ID, user lookup, shared registry, setting reads, notifications, and groups. `setup` can close over these dependencies to create its API.\n\nThe registry is shared metadata; `em` and actor belong to this execution. Plugins should retain stable provider configuration separately from request-bound managed records. The actor is optional here because scripts and public server reads can create scopes without a session."
+ },
+ {
+ "file": "packages/core/src/plugins.ts",
+ "pattern": "export type PluginApiMap<",
+ "title": "Infer API names and return types from the tuple",
+ "description": "The mapped type iterates over the union `TPlugins[number]`, remaps each member's key to its literal plugin ID, and infers its API type from `HackKitPlugin`.\n\nA known Teams tuple therefore gives `hackkit.plugins.teams` the API returned by `createTeamsApi`. Widening the tuple to a generic plugin array loses that specificity. This typing does not perform runtime discovery or verify permissions. The registry and API setup functions still enforce runtime behavior."
+ },
+ {
+ "file": "packages/core/src/plugins.ts",
+ "pattern": "const prefix = `\\$\\{plugin\\.id\\.replaceAll\\(\"-\", \"_\"\\)\\}_`;",
+ "title": "Enforce plugin namespaces during registration",
+ "description": "Entity table names must begin with the plugin ID plus an underscore, with hyphens converted to underscores. Settings and permission values use the plugin ID followed by a dot.\n\nFor Teams this yields `teams_...` tables and `teams....` setting/permission values. The registry also checks duplicates. These checks make ownership visible and reject accidental overlap before initialization. They do not create a sandbox around arbitrary plugin code."
+ },
+ {
+ "file": "packages/core/src/plugins.ts",
+ "pattern": "export function setupPluginApis",
+ "title": "Create each plugin API inside the current scope",
+ "description": "`setupPluginApis` walks the configured plugins and calls each enabled plugin's `setup(context)`. The result becomes the value at `hackkit.plugins[plugin.id]`; a plugin without setup gets an empty object.\n\nA disabled plugin receives an enumerable property getter that throws `INVALID_OPERATION` on access. Its entities, settings, and permissions remain registered. Disabling runtime behavior therefore does not request dropping its tables. The handler also omits disabled plugin endpoints."
+ },
+ {
+ "file": "packages/plugin-teams/src/index.ts",
+ "pattern": "export function teamsPlugin\\(\\)",
+ "title": "Declare Teams' complete contribution",
+ "description": "`teamsPlugin` returns ID `teams`, its npm package name, models, a maximum-team-size setting, permission declarations, endpoint factory, and `setup: createTeamsApi`.\n\nThe return type preserves both the ID and `ReturnType`. Adding a domain method to that factory can update the plugin API type automatically. A browser-callable operation also needs an endpoint and client usage; adding a factory method alone does not expose an HTTP route."
+ },
+ {
+ "file": "packages/plugin-teams/src/models.ts",
+ "pattern": "tableName: \"teams_member\",",
+ "title": "Enforce membership in the schema too",
+ "description": "The member entity maps to `teams_member`. Inspect its auth-user reference and uniqueness constraint. They complement the API's rule that a hacker can belong to only one team.\n\nThe adjacent team and invitation entities persist ownership and invitation state. These tables enter Core through `teamsPlugin().entities`; they are not discovered by scanning arbitrary source files. The host's migration must include them before the runtime can use them."
+ },
+ {
+ "file": "packages/plugin-teams/src/api.ts",
+ "pattern": "export function createTeamsApi",
+ "title": "Bind the Teams API to the scoped context",
+ "description": "The factory closes over `context.em` and returns methods. Helpers above it perform registered-hacker checks, membership lookup, HackTag resolution, team-capacity checks, and hydration of members with user data.\n\nThe declared permissions in the plugin descriptor are available for policy and role configuration. Individual Teams methods here enforce their actual rules through hacker membership and owner comparisons. Do not assume that declaring a permission automatically guards every method."
+ },
+ {
+ "file": "packages/plugin-teams/src/api.ts",
+ "pattern": "async createTeam\\(input:",
+ "title": "Create the team and owner membership together",
+ "description": "`createTeam` uses `teams.membership` as the lock key. It requires a registered hacker who is not already a member, normalizes the tag, rejects a duplicate tag, creates the team, and inserts its owner as a member.\n\nBoth flushes occur within the same transaction. The result hydrates the team with members and users for rendering. If a concurrent request tries to create another team for this owner, the serialized membership decision prevents both from succeeding."
+ },
+ {
+ "file": "packages/plugin-teams/src/api.ts",
+ "pattern": "async inviteToTeam\\(input:",
+ "title": "Resolve an invitation without prematurely admitting the user",
+ "description": "Only the team owner can invite. The method resolves the target from an auth ID or normalized HackTag, rejects self-invitation, existing membership, and a duplicate pending invitation, then persists a pending invite.\n\nThe target can be invited before completing hacker registration. Acceptance is where registration and current team capacity are enforced. An invitation is not a reserved team place."
+ },
+ {
+ "file": "packages/plugin-teams/src/api.ts",
+ "pattern": "async respondToInvite\\(input:",
+ "title": "Accept only the intended pending invitation",
+ "description": "The method locks the same membership key, loads the invite, verifies its recipient and pending status, and branches on acceptance. Declining updates the status without adding membership.\n\nAcceptance requires hacker registration, no existing team, and available capacity from `TeamsSetting.MaximumTeamSize`. It inserts membership and marks the invite accepted in the same transaction. A failed join must leave the losing invite consistent. This is the database result that the browser refresh will read."
+ },
+ {
+ "file": "packages/plugin-teams/src/endpoints.ts",
+ "pattern": "export function createTeamsEndpoints",
+ "title": "Adapt the scoped API to browser endpoints",
+ "description": "The endpoint factory receives `run`, not a prebuilt Teams API. It creates strict schemas for `/teams/create`, `/teams/invite`, invitation response, leaving, and member removal.\n\nEach operation gets `{ api, actorAuthId }` from the authenticated scope. The browser supplies fields such as name and tag; the endpoint supplies the trusted actor. Schemas exclude an actor field and reject extra properties. Continue with the HTTP chapter for the runner's session and scope creation."
+ },
+ {
+ "file": "packages/plugin-teams/src/components/team-create-form.tsx",
+ "pattern": "async function handleSubmit",
+ "title": "Follow the user action to the refreshed team page",
+ "description": "The form prevents normal submission, sets pending state, and invokes `@post/teams/create` with name and tag. `useHackkitClient` infers the legal path and body from endpoint types. Its type import does not load the endpoint implementation in the browser.\n\nFailure displays the structured error and stops. Success shows a toast and calls `router.refresh()`, causing the server page to read the committed team. The HTTP response does not install a global team store."
+ },
+ {
+ "file": "packages/plugin-teams/app/teams/page.tsx",
+ "pattern": "export default async function TeamsPage\\(\\)",
+ "title": "Render plugin-owned pages through the host runtime",
+ "description": "The page asks `getHackkitRuntime` for the host's current execution. It hides a disabled plugin, loads the current user, requires hacker registration, then reads team and owner-visible invites.\n\nThe result chooses a create form or team dashboard. This server read calls the domain API directly; it does not make an HTTP round trip to the host's own endpoint. The next chapter explains how Next discovers this package-owned page."
+ },
+ {
+ "file": "packages/plugin-teams/src/__tests__/teams.test.ts",
+ "pattern": "it\\(\"keeps the final team place and losing invite consistent across concurrent joins\"",
+ "title": "Check the last-place membership behavior",
+ "description": "Inspect the two competing invitation acceptances and assertions for membership and invite status. The nearby concurrent-creation test covers two creates by the same owner. Earlier tests cover ordinary creation and inviting before registration.\n\nTo add a Teams operation, update the API and its domain checks, schema/migration if needed, endpoint schema, and the relevant UI. Verify the user-visible result and failure behavior. Continue with [05. CLI and plugin page installation]."
+ }
+ ],
+ "nextTour": "05. CLI and plugin page installation"
+}
diff --git a/.tours/05-cli-route-sync.tour b/.tours/05-cli-route-sync.tour
new file mode 100644
index 000000000..70ff17ee6
--- /dev/null
+++ b/.tours/05-cli-route-sync.tour
@@ -0,0 +1,59 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "05. CLI and plugin page installation",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Goal: explain how package-owned Next pages become host routes and what `hackkit.lock` records. This is a filesystem generation process separate from Core's API registry and HTTP endpoint mounting.\n\nThese steps only inspect the sync implementation. Sync writes route files, so the tour does not run it automatically."
+ },
+ {
+ "file": "packages/cli/src/cli.ts",
+ "pattern": "#!/usr/bin/env node",
+ "title": "Find the CLI command boundary",
+ "description": "This executable is the CLI entry point. Follow its command declarations to configuration loading, plugin commands, and explicit database operations. The CLI operates relative to a selected project, which matters now that this repository has several templates and apps.\n\nCompare template package scripts before invoking a command from the repository root. A command's current directory controls which configuration and migration directory it loads."
+ },
+ {
+ "file": "packages/cli/src/plugin-manifest.ts",
+ "pattern": "export function resolvePluginPackage",
+ "title": "Resolve a plugin from the host application",
+ "description": "The resolver requires the plugin's `packageName`, creates a Node resolver rooted at the host's package.json, and finds the installed package root and version. It scans the package's `app` directory if present.\n\nEach discovered route becomes a host path and package import source. A plugin without pages can still resolve with an empty route list. This mechanism relies on package exports and installed dependencies, not the Core API map."
+ },
+ {
+ "file": "packages/cli/src/protected-paths.ts",
+ "pattern": "export function discoverPluginRouteFiles",
+ "title": "Discover only Next route files",
+ "description": "The recursive walker keeps filenames that match known Next route conventions such as page, layout, loading, and route. It normalizes separators and sorts results.\n\nComponent files beside a page are not copied into the host. Generated files import the original package, so implementation and relative dependencies stay with that package. Review the package's exports when adding a route so the generated import can resolve."
+ },
+ {
+ "file": "packages/cli/src/plugin-sync.ts",
+ "pattern": "function collectRouteClaims",
+ "title": "Reject protected-path and cross-plugin conflicts first",
+ "description": "Before writing, sync builds a map of claimed host paths. It rejects any route under a protected host path and rejects two different plugins claiming the same route.\n\nThe default protected list includes Core pages and `app/api`, keeping browser endpoints under the central handler rather than plugin route stubs. These are the explicit conflict checks here. Do not infer a general guarantee that all existing handwritten files are protected; the later write loop writes each allowed generated destination."
+ },
+ {
+ "file": "packages/cli/src/plugin-sync.ts",
+ "pattern": "function renderRouteStub",
+ "title": "Generate a reexport instead of copying page code",
+ "description": "The stub includes a generated marker, owner/version comment, and `export *` from the package route module. For route filenames with a default component export, it adds the default reexport too.\n\nThe host route is small because the plugin owns the implementation. A handler route would have named method exports, but the default path protection rejects plugin claims under host `app/api`. Runtime API endpoints use the endpoint factory path explained in the HTTP chapter."
+ },
+ {
+ "file": "packages/cli/src/plugin-sync.ts",
+ "pattern": "export async function runPluginSync",
+ "title": "Track current routes and remove stale generated stubs",
+ "description": "`runPluginSync` reads the previous lockfile, resolves configured plugins, validates claims, writes current stubs, and writes the new lockfile. It removes an old route only when it is no longer claimed and its contents still start with the generated marker.\n\nThe result reports routes written and stubs removed. The write loop is filesystem generation, not a database transaction. Inspect proposed route destinations before syncing a new third-party plugin. This implementation does not generate HTTP clients or per-endpoint route wrappers."
+ },
+ {
+ "file": "templates/nextjs-full/hackkit.lock",
+ "pattern": "\"version\": 1",
+ "title": "Read the generated ownership record",
+ "description": "The lockfile records each plugin's ID, package, version, and generated route mappings. Compare Teams' route entries with its package `app` directory and the full template's route reexports.\n\nThis file tracks generated route ownership. It is separate from pnpm-lock.yaml, which locks package resolution, and separate from database migration history. The E2E preparation checks that syncing these routes leaves committed output unchanged."
+ },
+ {
+ "file": "packages/cli/src/orm.ts",
+ "pattern": "export async function withOrm",
+ "title": "Keep CLI database connections bounded",
+ "description": "Database commands use the same `createOrmOptions` function as runtime startup. `withOrm` adds Jiti-based imports for native TypeScript migration files and closes the ORM in `finally` after the operation finishes or throws.\n\nMigration generation writes a reviewable migration. Applying migrations is a separate command. Continue with [06. HTTP, Next integration, and browser UI] to trace the runtime request path."
+ }
+ ],
+ "nextTour": "06. HTTP, Next integration, and browser UI"
+}
diff --git a/.tours/06-request-flow.tour b/.tours/06-request-flow.tour
new file mode 100644
index 000000000..f02423b19
--- /dev/null
+++ b/.tours/06-request-flow.tour
@@ -0,0 +1,113 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "06. HTTP, Next integration, and browser UI",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Goal: follow a browser mutation through JSON validation, origin checks, session resolution, a fresh Core scope, domain execution, and a typed response. Then compare that path with server-rendered reads.\n\nUse the Teams create request from chapter 04 as the running example. A request body contains the requested change. The session determines who requested it."
+ },
+ {
+ "file": "templates/nextjs-full/app/api/hackkit/[...path]/route.ts",
+ "pattern": "export const POST",
+ "title": "Mount the central HackKit handler",
+ "description": "The catch-all route exports the application's handler as POST. Endpoint definitions stay in Core and plugins rather than one generated Next route per operation.\n\nBetter Auth has its own catch-all auth route. File upload/view routes remain host routes because they handle storage and access policy outside this JSON mutation handler. Next passes a Request to the host, which initializes the reusable endpoint router when needed."
+ },
+ {
+ "file": "packages/next/src/runtime.ts",
+ "pattern": "export function createHackkitRuntimeFromConfig",
+ "title": "Create the Next host and initialize lazily mounted transport",
+ "description": "The host normalizes config, requires an explicit auth base URL, and accepts a static trusted-origins list. It starts Core initialization with Better Auth's `nextCookies` plugin.\n\nIt shares the Core promise and mounts the HTTP handler on first use. `getRuntime` is wrapped with React `cache`, whose reuse belongs to the server render. This does not authorize storing the current user's runtime globally. The global integration registration stores a provider function."
+ },
+ {
+ "file": "packages/core/src/http/handler.ts",
+ "pattern": "export function createHackkitHandler",
+ "title": "Build endpoint definitions once",
+ "description": "The handler constructs a request runner, mounts Core endpoints, and asks each enabled plugin's endpoint factory for its definitions. It rejects plugin paths outside `//`, non-POST endpoints, and duplicate POST paths.\n\nOnly definitions and shared runtime resources persist in this handler. A plugin factory receives the runner so its eventual callback can obtain a fresh scope. The default base path is `/api/hackkit`."
+ },
+ {
+ "file": "packages/core/src/http/http-server.ts",
+ "pattern": "export function protectHttpHandler",
+ "title": "Reject untrusted mutation origins before dispatch",
+ "description": "The outer wrapper requires an allowed Origin for methods other than GET/HEAD and rejects requests marked cross-site. A nonempty mutation body must use application/json. Empty POST streams receive special handling because Next can expose a stream for a bodyless request.\n\nThis policy runs before router dispatch. It reduces cross-site mutation risk but does not replace session authentication or domain permissions. The configured origin must describe the actual host that makes browser requests."
+ },
+ {
+ "file": "packages/core/src/http/handler.ts",
+ "pattern": "const run: RequestRunner = async \\(request, operation\\) => \\{",
+ "title": "Resolve the actor from a fresh session lookup",
+ "description": "For an endpoint operation, the runner asks Better Auth for a session with cookie cache disabled and requests response headers. No session produces `UNAUTHORIZED`.\n\nIt takes `session.response.user.id` as actorAuthId, then calls `runtime.run` to create a fresh domain scope. The callback receives that scope's `hackkit` plus the trusted actor. Session headers are stored per Request and later propagated as Set-Cookie headers. They are not shared actor state."
+ },
+ {
+ "file": "packages/core/src/http/mutation.ts",
+ "pattern": "export function createMutationFactory",
+ "title": "Connect validated bodies to domain operations",
+ "description": "`createMutationFactory(run)` returns a typed POST endpoint builder. Each endpoint declares a literal path, Zod body schema, and callback. Better-call parses the body before the callback uses `context.body`.\n\nThe endpoint invokes the request runner, passes the authenticated scope to the operation, then converts the domain result to its JSON DTO. Registration endpoints omit authId/userId from browser schemas and insert the session ID. That is how transport prevents a caller from choosing another participant as themselves."
+ },
+ {
+ "file": "packages/core/src/http/endpoints/registration.ts",
+ "pattern": "export function createRegistrationEndpoints",
+ "title": "Bind personal writes to the session identity",
+ "description": "The personal-data schema omits authId and the hacker-registration schema omits userId. Both become strict browser schemas. Their callbacks insert the session's actorAuthId into the Core call.\n\nA browser cannot nominate another user's ID in these bodies or smuggle one through an extra field. Direct server calls use Core's target-bearing inputs, so server code remains responsible for choosing a trusted identity when it bypasses this adapter."
+ },
+ {
+ "file": "packages/core/src/http/mutation.ts",
+ "pattern": "export function createPluginMutationFactory",
+ "title": "Resolve the plugin API inside the authenticated scope",
+ "description": "The plugin mutation factory wraps the Core mutation builder. It verifies the plugin path prefix and retrieves `scope.hackkit.plugins[plugin.id]` during execution. The API therefore uses the current request's EntityManager.\n\nThe setup factory in the declaration provides the API type used for inference. A small type assertion connects the dynamic registry lookup to that contract. Runtime plugin enablement and route validation still matter; compile-time inference cannot prove a deployed plugin is enabled."
+ },
+ {
+ "file": "packages/core/src/http/http-contract.ts",
+ "pattern": "export function toJsonDto",
+ "title": "Make the declared wire type match JSON",
+ "description": "`toJsonDto` serializes and parses the domain value, converting Date values to strings and void/undefined results to null. The recursive `Wire` type models these conversions for client inference.\n\nA server DTO may contain Date instances while a browser receives ISO strings. UI code must parse a date when it needs Date methods. Matching the inferred endpoint output to the actual JSON avoids presenting browser callers with impossible runtime types."
+ },
+ {
+ "file": "packages/core/src/http/http-server.ts",
+ "pattern": "export function httpError",
+ "title": "Return useful domain errors and hide exception details",
+ "description": "Known HackKit errors map to stable HTTP statuses and `{ code, message }`. Expected Better-call errors retain useful client messages. Unexpected exceptions become a generic 500 response, while the server callback logs details.\n\nThe final response policy below normalizes router results, applies JSON and no-store headers, and handles empty success responses. The client can therefore handle domain failure through its structured result instead of parsing arbitrary exception text."
+ },
+ {
+ "file": "packages/core/src/http/client.ts",
+ "pattern": "export function createHackkitClient",
+ "title": "Infer paths without bundling endpoint implementations",
+ "description": "The browser client combines Core endpoint types with any plugin endpoint types supplied by the caller. It defaults to `/api/hackkit`, same-origin credentials, no retries, and errors returned through the result.\n\nType-only imports connect inference to server declarations without executing those modules in the browser. The transport still makes a real HTTP request. A path such as `@post/teams/create` selects a declared route and infers its body and response."
+ },
+ {
+ "file": "packages/ui/src/provider.tsx",
+ "pattern": "export function useHackkitClient",
+ "title": "Share client settings and navigation through React context",
+ "description": "`HackKitUIProvider` holds the HTTP client, navigation adapter, and event-pass lifetime. Without an override it uses Next's router and the resolved route map. `useHackkitClient` creates a typed client using the provider's same options.\n\nPlugin forms can add endpoint types without replacing the application's provider. Shared Core components under `packages/ui/src/components` use the same transport and error helpers. The provider stores UI dependencies, not an EntityManager or auth secret."
+ },
+ {
+ "file": "packages/next/src/runtime.ts",
+ "pattern": "export async function createHackkitRuntime<",
+ "title": "Compare server-rendered reads with HTTP mutations",
+ "description": "The server-render runtime reads Next request headers, resolves the session, and creates a scope. It exposes `getAuthId`, `getCurrentUser`, settings reads, and page guards. Unauthenticated required identity redirects to sign-in.\n\nServer pages call these domain APIs directly. They do not need to fetch their own browser endpoint. Unlike the HTTP runner, this function directly creates a scope rather than calling `runtime.run`; do not describe these two paths as identical RequestContext execution."
+ },
+ {
+ "file": "packages/next/src/page-guards.ts",
+ "pattern": "async function requireParticipantAccess\\(\\)",
+ "title": "Gate pages with onboarding and approval state",
+ "description": "`requireParticipantAccess` requires completed onboarding and approval. Its helper checks a current user, bans, HackTag, personal data, and hacker registration, redirecting to the appropriate step when something is missing.\n\nPermission guards call Core access control and translate forbidden access into the configured Next navigation behavior. These guards control page entry. Domain mutations still need their own rules when accessed directly over HTTP."
+ },
+ {
+ "file": "packages/core/src/http/event-pass.ts",
+ "pattern": "export function resolveEventPassTargetAuthId",
+ "title": "Decode the event pass and bound its age",
+ "description": "The pass payload contains authId and qrIssuedAt as JSON. Parsing checks field types, then validates the timestamp against a lifetime and a one-minute future tolerance. The resolver returns the target auth ID for scanner endpoints.\n\nThis is a timestamped identifier payload, not a cryptographic signature. Scanner authorization and target checks still run on the server. Changing its security properties would require changing both the payload producer and server validation, not just the QR renderer."
+ },
+ {
+ "file": "packages/core/src/http/endpoints/event-pass.ts",
+ "pattern": "export function createEventPassEndpoints",
+ "title": "Authorize scanner preview before returning participant data",
+ "description": "The preview endpoint chooses EventsScan or UsersCheckIn permission according to whether an eventId is supplied. It reads the configured QR lifetime, resolves the target, loads the user, and optionally loads prior event scans.\n\nThe browser's raw QR text identifies a target; it does not authorize the scanner. Domain check-in and event-scan endpoints perform the final writes. Keep preview and mutation behavior distinct when adding scan warnings."
+ },
+ {
+ "file": "tooling/check-client-bundle.mjs",
+ "pattern": "const forbidden = files\\.filter",
+ "title": "Check the actual provider bundle for server dependencies",
+ "description": "The script bundles providers.tsx for both templates and the demo using browser-targeted esbuild. It inspects metafile input paths and rejects MikroORM, Better Auth, and specified server implementation modules. It prints bundle size and module counts for each target.\n\nThis verifies the dependency boundary for these entry points. It does not prove every possible browser entry is safe, that a production Next build passes, or that permissions work. Use it alongside the relevant behavior checks."
+ }
+ ],
+ "nextTour": "07. Notifications, Discord, storage, and deployed apps"
+}
diff --git a/.tours/07-integrations-apps.tour b/.tours/07-integrations-apps.tour
new file mode 100644
index 000000000..998ff60cf
--- /dev/null
+++ b/.tours/07-integrations-apps.tour
@@ -0,0 +1,107 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "07. Notifications, Discord, storage, and deployed apps",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Goal: see how Core supports external services without requiring every application to configure them. Trace durable notification intents into email delivery, Discord linking into role synchronization, and host storage routes into adapters.\n\nThe final steps distinguish the deployed demo and documentation apps from reference templates. Reading this chapter does not contact providers or verify a hosted deployment."
+ },
+ {
+ "file": "packages/core/src/notifications.ts",
+ "pattern": "async queueIntent\\(",
+ "title": "Store delivery intent with the domain change",
+ "description": "`queueIntent` validates the notification kind/payload and optionally returns an existing intent by idempotency key. Otherwise it creates a pending row and flushes it through the current EntityManager.\n\nWhen a domain transaction calls this method, the intent is part of that database transaction. A committed RSVP can therefore leave durable work for a later delivery call. Queueing an intent does not send email or start a background worker."
+ },
+ {
+ "file": "packages/core/src/notifications.ts",
+ "pattern": "async deliverPending\\(",
+ "title": "Claim work and record delivery attempts",
+ "description": "The delivery loop reads pending intents and conditionally changes each to processing. A zero-row claim means another execution already claimed it. Each supplied channel runs, and success, skip, or failure becomes a delivery-attempt record. Intent status is then summarized.\n\nThe method is explicit: a host or job must call it. Do not assume a scheduler, automatic retry, or exactly-once provider delivery from this loop. A process crash between claiming and recording results needs operational consideration beyond the code shown here."
+ },
+ {
+ "file": "packages/plugin-notifications-email/src/index.ts",
+ "pattern": "export function emailNotificationsPlugin",
+ "title": "Keep provider configuration separate from scoped email APIs",
+ "description": "The email plugin has a setup API but no entity or endpoint declarations. It uses Core's notification tables and can accept options or a deferred options factory.\n\nThe resolved options are cached in the plugin closure, while `createEmailNotificationsApi` runs with each scope's context. Stable provider settings can therefore be reused without retaining a request's EntityManager. The plugin ID is `notificationsEmail`."
+ },
+ {
+ "file": "packages/plugin-notifications-email/src/api.ts",
+ "pattern": "export function createEmailNotificationsApi",
+ "title": "Render and deliver one email channel",
+ "description": "The channel checks for a provider and a template, resolves a recipient through scoped user lookup, renders the template, and calls the provider. Missing provider/template/recipient returns a skipped result. Successful sends include provider and external message metadata.\n\n`deliverPending` delegates to Core with this channel. `deliverIntent` delivers one selected intent and records an attempt. SMTP and Resend implementations live in providers.ts; domain registration and RSVP code do not import either provider."
+ },
+ {
+ "file": "packages/plugin-discord/src/index.ts",
+ "pattern": "export function discordPlugin",
+ "title": "Compose Discord persistence and API behavior",
+ "description": "Discord declares its own models, endpoint factory, package name, and scoped setup factory. Deferred configuration is resolved once and reused for new APIs, as in the email plugin.\n\nIts persisted verification/member/sync-attempt records are separate from the external Discord service. A local link can be committed even when a later provider call fails. Inspect the next steps for that ordering."
+ },
+ {
+ "file": "packages/plugin-discord/src/api.ts",
+ "pattern": "async createVerification\\(",
+ "title": "Issue a pending verification with a bounded lifetime",
+ "description": "The method requires the configured guild, persists the code and Discord identity with an expiration time, and returns a verification URL. The browser link is a way to associate that pending verification with a signed-in HackKit participant.\n\nCreating the pending row does not approve a hacker or grant roles. Confirmation must verify current status, time, and ownership constraints before creating the member link."
+ },
+ {
+ "file": "packages/plugin-discord/src/api.ts",
+ "pattern": "async confirmVerification\\(",
+ "title": "Commit the account link before contacting Discord",
+ "description": "Confirmation uses a verification lock and rejects missing/non-pending codes. It requires an approved registered hacker and prevents a Discord account from linking to a different user.\n\nExpiration and conflicting-link branches return an error value from the transaction so their expired/rejected status changes can commit, then throw afterward. Successful confirmation commits the member link and accepted code before `syncMemberRoles` contacts the provider. A network failure therefore does not roll back an already-consumed code."
+ },
+ {
+ "file": "packages/plugin-discord/src/api.ts",
+ "pattern": "async syncMemberRoles\\(input:",
+ "title": "Separate role planning from provider delivery",
+ "description": "The method builds a plan from linked member and participant/group information. Without a provider it records a skipped attempt. With a provider it sends role IDs/names and nickname, updates last-sync time after success, and records the result.\n\nProvider errors become failed attempts. They do not delete the local member link. The provider module handles HTTP communication; the plan remains inspectable through the plugin API."
+ },
+ {
+ "file": "templates/nextjs-full/lib/blob.ts",
+ "pattern": "export function createBlobStorageFromConfig",
+ "title": "Choose a storage adapter at the host boundary",
+ "description": "The full template chooses S3-compatible storage or local filesystem storage according to application config. The shared interface exposes upload targets and object reads. Additional capabilities let the host distinguish a local write path from a signed S3 view URL.\n\nStorage adapters do not know whether a user may view a resume. That decision stays in the host route. The Core profile stores a stable reference rather than depending on a temporary signed URL."
+ },
+ {
+ "file": "packages/blob-local/src/index.ts",
+ "pattern": "export function createLocalBlobStorage",
+ "title": "Turn a local upload request into a stable reference",
+ "description": "The adapter generates a location/UUID key and returns the host's upload URL plus `/api/files/view?key=...` as the stored reference. It exposes directory preparation and filesystem reads for host routes.\n\nRead the path normalization and base-directory checks before changing key handling. The host must still validate accepted locations and access. The adapter's methods are storage operations, not a complete upload authorization policy."
+ },
+ {
+ "file": "packages/blob-s3/src/index.ts",
+ "pattern": "export function createS3BlobStorage",
+ "title": "Use temporary S3 URLs with stable application references",
+ "description": "The S3 adapter creates signed PutObject URLs and returns a stable host view reference. It can read objects or create a signed GetObject URL. Signing expiry defaults to one hour unless overridden.\n\nThe browser can upload to the signed destination while application records keep the durable reference. Credentials stay in server configuration. A signed URL grants temporary object access, so the host checks permissions before returning one for private files."
+ },
+ {
+ "file": "templates/nextjs-full/app/api/files/view/route.ts",
+ "pattern": "async function canViewFile",
+ "title": "Apply file access policy before storage access",
+ "description": "Profile photos are public under this route's policy. Other files require a session. A participant can view their registered resume reference; a user with `HackersView` can view other resumes.\n\nGET also restricts accepted key prefixes. After permission checks, S3 redirects to a signed view URL and local storage returns bytes. File privacy therefore depends on this host route as well as bucket/filesystem configuration."
+ },
+ {
+ "file": "apps/demo/lib/runtime.ts",
+ "pattern": "function getHost\\(\\)",
+ "title": "Reuse packages in the demo with guarded lazy initialization",
+ "description": "The demo creates its host lazily after asserting demo environment configuration, then registers a provider for package-owned pages. Its convenience API mirrors the templates while using its own config and seeded fictional event data.\n\nThe demo is a deployed app with its own build, database scripts, and bindings. It is not the target for template E2E. Inspect apps/demo/README.md for local setup and deployment rather than inheriting the full template's database assumptions."
+ },
+ {
+ "file": "apps/demo/lib/database-config.ts",
+ "pattern": "export const databaseOptions = \\{",
+ "title": "Adapt MikroORM to HTTP libSQL for the Worker",
+ "description": "The demo uses the generic SQLite driver from `@mikro-orm/sql` with a custom Kysely HTTP libSQL dialect. This avoids depending on a native SQLite binary in the Worker.\n\nThe dialect gets its URL/token from demo environment configuration. Reference templates can choose other driver configurations. Similar domain APIs do not imply identical deployment transports or database drivers."
+ },
+ {
+ "file": "apps/demo/lib/http-libsql-dialect.ts",
+ "pattern": "override async acquireConnection\\(\\)",
+ "title": "Keep HTTP stream state within one connection checkout",
+ "description": "Each acquired connection gets a fresh web libSQL client. The driver tracks it in a WeakMap and closes it on release. A transaction retains its connection while unrelated requests acquire their own.\n\nThe same file implements savepoint, rollback-to-savepoint, and release-savepoint operations for nested transactions. Its adapter permits multiple connections. These details let Core's transaction behavior work through the demo's HTTP database transport."
+ },
+ {
+ "file": "apps/docs/lib/source.ts",
+ "pattern": "const docs = defineDocs\\(\\{",
+ "title": "Keep the documentation site separate from the domain runtime",
+ "description": "Fumadocs reads MDX from content/docs, applies page/meta schemas, and constructs the site source. The loader exposes routes and processed Markdown for the docs' text representation.\n\nThis app teaches and documents HackKit; it does not implement registration or plugin composition. The docs' package/build configuration differs from the reference templates. Continue with [08. Verification and change guide] for the repository's test boundaries."
+ }
+ ],
+ "nextTour": "08. Verification and change guide"
+}
diff --git a/.tours/08-verification.tour b/.tours/08-verification.tour
new file mode 100644
index 000000000..e5bd0a109
--- /dev/null
+++ b/.tours/08-verification.tour
@@ -0,0 +1,86 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "08. Verification and change guide",
+ "steps": [
+ {
+ "title": "What you will learn",
+ "description": "Goal: choose the right evidence for a Core, plugin, template, or deployment change. This chapter explains what package tests, database checks, production HTTP tests, browser journeys, and deployment checks actually cover.\n\nTour validation checks source anchors and links. It does not imply the application suites pass. The suite commands below are read-only text in the tour, with no automatic terminal actions."
+ },
+ {
+ "file": "packages/core/src/testing.ts",
+ "pattern": "export async function createTestHackkit",
+ "title": "Use an isolated real database for focused domain tests",
+ "description": "The helper initializes Core against in-memory SQLite, injects a deterministic Core clock and ID generator, creates the schema, and returns a scoped API with access to the runtime and EntityManager. Vitest cleanup closes registered runtimes after each test.\n\n`createTestUser` can insert a known identity without password hashing when a test concerns domain behavior. This is useful focused evidence, but it bypasses auth signup and committed migrations. The auth lifecycle and application E2E tests cover those other boundaries."
+ },
+ {
+ "file": "e2e/support/templates.mjs",
+ "pattern": "export const templates = \\{",
+ "title": "Point integration tests at the real templates",
+ "description": "The manifest names the full and minimal workspace packages, absolute directories, fixed ports, origins, and database dialect lists. Full targets SQLite/libSQL/PostgreSQL/MySQL; minimal targets SQLite.\n\nHelpers use these absolute locations rather than assuming the repository root is an application. This is the main architectural effect of the testing split: tests orchestrate real consumers and their committed migrations."
+ },
+ {
+ "file": "e2e/support/templates.mjs",
+ "pattern": "export function testEnvironment",
+ "title": "Build explicit disposable test configuration",
+ "description": "The helper constructs a limited environment with fresh database/upload paths, template-specific origins, and a test auth secret. Full-template external-service settings use disabled or non-service test values so Next's dotenv loading cannot silently select developer services.\n\nRead which variables are intentionally inherited and which are replaced. Test isolation depends on the actual environment passed to each subprocess, not just a temporary directory name."
+ },
+ {
+ "file": "e2e/support/run.mjs",
+ "pattern": "const suite = process\\.argv\\[2\\] \\?\\? \"all\";",
+ "title": "Prepare dependencies before choosing integration suites",
+ "description": "The runner accepts all, db, http, or browser. It builds dependency packages, snapshots generated route files and lockfiles, runs plugin sync, and asserts no generated drift. It then typechecks both templates and E2E and invokes the client-bundle guard.\n\nHTTP/browser runs also migrate and seed disposable build databases and create real production template builds. Finally it runs the selected Vitest or Playwright suites. Running one E2E invocation at a time avoids collisions in fixed ports and template output directories."
+ },
+ {
+ "file": "e2e/support/server.mjs",
+ "pattern": "export async function startTemplate",
+ "title": "Start production Next with fresh storage",
+ "description": "The helper rejects an occupied port, creates a temporary runtime directory, and runs migrations and seeding with the template as cwd. It starts `next start`, captures logs, and polls the auth session endpoint for readiness.\n\nIt never treats an arbitrary process on the same port as the test application. The returned close function stops the child and removes temporary storage. Startup failure also performs cleanup and includes captured output in its error."
+ },
+ {
+ "file": "e2e/playwright.config.ts",
+ "pattern": "projects: \\[\"full\", \"minimal\"\\]\\.flatMap",
+ "title": "Run transport and UI checks for each consumer",
+ "description": "Playwright creates full-http, full-browser, minimal-http, and minimal-browser projects. One worker and disabled full parallelism match the shared production output and fixed-port strategy.\n\nFailure traces, screenshots, HTML, and JUnit output support diagnosis. Retries are zero here, so intermittent behavior appears as a failure instead of silently passing a retry. Project selection determines coverage; a browser-only run does not execute the database matrix."
+ },
+ {
+ "file": "e2e/http/runtime.spec.ts",
+ "pattern": "test\\(\"production HTTP auth, request isolation, permissions, Core and plugin transport\"",
+ "title": "Exercise the complete HTTP boundary",
+ "description": "Read the requests and assertions for sessions, actor isolation, invalid input, permissions, and plugin transport. These tests use a production template server rather than calling a domain function in memory.\n\nThe local-upload test later in this file checks minimal-template file round trips and resume access. Compare these assertions with the handler and file-route implementation before claiming a transport or authorization change is covered."
+ },
+ {
+ "file": "e2e/browser/participant.spec.ts",
+ "pattern": "test\\(\"signup, onboarding, persisted registration, signout and signin\"",
+ "title": "Check the participant journey through rendered UI",
+ "description": "This browser journey signs up, completes onboarding, verifies persisted registration, signs out, and signs back in. Additional tests cover anonymous/incomplete-account redirects, approval waiting, participant denial from admin pages, and owner administration.\n\nThe browser assertions validate the integration among UI, Next routing, auth cookies, Core behavior, and storage. A package unit test alone cannot prove these rendered transitions work."
+ },
+ {
+ "file": "e2e/browser/teams.spec.ts",
+ "pattern": "test\\(\"team creation, invitation and acceptance across separate sessions\"",
+ "title": "Close the worked Teams scenario",
+ "description": "This journey exercises team creation, invitation, and acceptance across separate browser sessions. Compare each action with the endpoint/API methods from chapter 04.\n\nTeams belongs to the full configuration; inspect the template-selection condition in this test before interpreting a skip for minimal. A skip because an optional feature is absent is different from demonstrating that feature works in that consumer."
+ },
+ {
+ "file": "e2e/database/templates.test.ts",
+ "pattern": "it\\(\"minimal: real config and committed migrations initialize Core/auth without plugins\"",
+ "title": "Validate the minimal schema without feature plugins",
+ "description": "This test loads the real minimal config and committed migrations, then initializes Core/auth without feature plugins. It checks the split at the application/configuration boundary.\n\nThe parameterized test above covers storage isolation, sync without connecting, and reset restrictions. Other files in e2e/database cover dialect and migration workflows. Inspect matrix environment requirements before treating skipped network dialect tests as verified coverage."
+ },
+ {
+ "file": ".github/workflows/integration.yml",
+ "pattern": "HACKKIT_REQUIRE_DATABASE_MATRIX: \"1\"",
+ "title": "Require the database matrix in CI",
+ "description": "The integration workflow provisions PostgreSQL, MySQL, and libSQL services, supplies their test URLs, and requires the matrix for its complete E2E invocation. It also verifies generated template route files and lockfiles have no diff.\n\nThe separate package job runs library tests and package typechecks. These are workflow definitions, not evidence of a successful run on this working tree. Check the actual CI result for the branch before making a release claim."
+ },
+ {
+ "file": ".github/workflows/apps.yml",
+ "pattern": "name: Demo and documentation",
+ "title": "Check deployed-app packaging separately",
+ "description": "The app workflow builds/typechecks docs and the demo, checks deployment packaging, and uses disposable libSQL plus local Worker bindings for the demo's database and smoke checks.\n\nTemplate E2E does not replace these checks because Worker bindings and the demo dialect differ from Next template hosting. Likewise a local deploy check does not prove a hosted URL has the intended version. Verify hosted behavior separately when publishing is requested."
+ },
+ {
+ "title": "Choose the change location and finish the route",
+ "description": "You have now followed configuration into shared initialization, scoped Core/plugin APIs, HTTP transport, server pages, storage, deployed apps, and test orchestration.\n\nFor a Core rule, start in `packages/core/src/functions`, then check the domain schema and focused behavior tests. For a new plugin capability, add its scoped API and checks, endpoint schema if browser access is needed, and UI. Change entities plus a reviewed host migration when persistence changes. Use CLI sync only for package-owned page installation.\n\nFor template behavior, compare full and minimal configurations and choose the relevant E2E journey. For deployment behavior, inspect the app's own runtime/bindings and app workflow. The root commands are `corepack pnpm test` for package tests and `corepack pnpm test:e2e` for the complete template suite; E2E builds production output and writes disposable databases. Read e2e/README.md before running it.\n\nRevisit [01. Repository map and composition], [03. Core domain behavior], or [04. Plugin contracts and Teams] when choosing the layer to change. Tour playback and source-anchor validation do not execute these application checks."
+ }
+ ]
+}
diff --git a/.tours/README.md b/.tours/README.md
new file mode 100644
index 000000000..b9f3e36d2
--- /dev/null
+++ b/.tours/README.md
@@ -0,0 +1,26 @@
+# HackKit repository walkthrough
+
+Open the repository root in VS Code with the CodeTour extension enabled. In the Explorer's CodeTour view, start **01. Repository map and composition**. It is the primary tour. Each chapter names the next tour, and linked tour titles let you jump between topics.
+
+These tours describe the `templates-and-testing` working tree, including its uncommitted template/E2E split. They do not pin a Git ref. Paths and regex anchors refer to the current files. Read them with the repository root as the workspace folder.
+
+The walkthrough has 8 chapters and 117 steps. Each step explains the role of the selected code, its inputs and outputs, important checks, and the next boundary where relevant. No step runs a command or edits source automatically.
+
+- [01. Repository map and composition](01-repository-map.tour) (11 steps)
+- [02. Core startup and persistence](02-core-startup.tour) (13 steps)
+- [03. Core domain behavior](03-core-domain.tour) (19 steps)
+- [04. Plugin contracts and Teams](04-plugin-system.tour) (16 steps)
+- [05. CLI and plugin page installation](05-cli-route-sync.tour) (9 steps)
+- [06. HTTP, Next integration, and browser UI](06-request-flow.tour) (18 steps)
+- [07. Notifications, Discord, storage, and deployed apps](07-integrations-apps.tour) (17 steps)
+- [08. Verification and change guide](08-verification.tour) (14 steps)
+
+The Core chapters explain shared initialization, per-execution EntityManagers, canonical auth users, registration, role hierarchy, settings, RSVP, check-in, groups, and transaction locking. The plugin chapter follows Teams through declarations, inferred API types, scoped setup, persistence, endpoints, forms, and server pages. Later chapters explain CLI route sync, request handling, notifications, Discord, storage, the demo Worker, docs, and the test suites.
+
+For a focused first pass on the plugin system, read chapters 01, 02, 04, 05, and 06. For participant behavior, read chapters 02, 03, 06, and 08.
+
+## Validation boundaries
+
+The CodeTour anchor checker verifies all local files and unique regex matches. Additional checks verify tour sequencing, tour-title links, local Markdown links, and the installed extension's JSON schema. Source review checks descriptions against implementation. Editor playback has not been exercised, so discovery, comment placement, and rendered links still need a VS Code playback check.
+
+Application tests are described as evidence to inspect. They were not run for this documentation-only change. Creating the tours does not verify current service credentials, deployment state, or hosted behavior. The tours do not install an extension or change workspace trust.
diff --git a/README.md b/README.md
index cf74a5dac..e5d438b40 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
HackKit
Feature-packed Hackathon Managment Software That Just Works.
Putting together a hackathon is not an easy feat, like really not easy. While we can't help you with delayed packages, food orders being botched, or any of the other issues that can arise at the worst times, we can help you with the tech to make your hackathon go smoothly. HackKit is battle-tested and feature-packed to help your hackathon be the best it can be. From a seamless registration, to an easy-to-use admin panel, to event passes and more, HackKit has you covered.
-
Want to get started with using HackKit? Check out our OSS Documents that will be updated as we add new features and fix bugs!
+
Want to get started with using HackKit? Check out our documentation that will be updated as we add new features and fix bugs!
@@ -16,9 +16,26 @@
- Better Auth
- Tailwind CSS
+## Demo and documentation
+
+- [Interactive demo](https://hackkit-demo.acm-utsa.workers.dev): explore a shared fictional event as a participant or organizer. [Deployment and local setup](apps/demo/README.md).
+- [Documentation](https://hackkit-docs.acm-utsa.workers.dev): setup, templates, architecture, plugins, operations, and testing. [Contributing to the docs](apps/docs/README.md).
+
+Configure the demo’s Turso credentials in `apps/demo/.dev.vars` and run its explicit database setup first; see [demo setup](apps/demo/README.md). `corepack pnpm dev:demo` then starts the demo on port 3001. `corepack pnpm dev:docs` starts Fumadocs on port 3002. The deployed apps live under `apps`; runnable workspace examples remain under `templates`.
+
## Development
-See [the reference app setup](apps/web/README.md) and [the persistence decision](docs/adr/0013-mikroorm-and-core-auth.md). This migration targets fresh databases.
+Use Node.js 22.17+ and the repository-pinned pnpm 12.15.0 through Corepack. Run `corepack pnpm install` from this directory.
+
+- [Full Next.js template](templates/nextjs-full/README.md): the existing app with Teams, Discord, email notifications, and configurable storage.
+- [Minimal Next.js template](templates/nextjs-minimal/README.md): Core registration, participant profiles, and basic administration with local SQLite and uploads.
+- [E2E validation](e2e/README.md): database, production HTTP, and browser integration checks against these templates.
+
+`corepack pnpm dev` and `corepack pnpm dev:full` start the full template. `corepack pnpm dev:minimal` starts the minimal template. Root `db:*` commands target the full template. Select the minimal package explicitly for its database commands. Each template's README covers migrations, seeding, owner bootstrap, and production configuration.
+
+`corepack pnpm build` builds libraries, templates, demo, and docs; configure each template for production first. `corepack pnpm test` runs package tests. `corepack pnpm test:e2e` prepares isolated configurations and runs all integration suites. Run one E2E invocation at a time because tests use fixed ports and each template's production output.
+
+Both templates remain workspace examples. [CLI init and standalone releases](docs/plans/template-distribution.md) are a separate milestone. See the [persistence decision](docs/adr/0013-mikroorm-and-core-auth.md) for database ownership.
## Join The Team
diff --git a/apps/demo/.dev.vars.example b/apps/demo/.dev.vars.example
new file mode 100644
index 000000000..91f98c179
--- /dev/null
+++ b/apps/demo/.dev.vars.example
@@ -0,0 +1,6 @@
+# Copy this file to .dev.vars in this directory. Never commit .dev.vars.
+TURSO_DATABASE_URL=libsql://your-database.turso.io
+TURSO_AUTH_TOKEN=
+BETTER_AUTH_SECRET=
+# Local application origin. The deployed origin lives in wrangler.jsonc.
+BETTER_AUTH_URL=http://localhost:3001
diff --git a/apps/demo/.env.example b/apps/demo/.env.example
new file mode 100644
index 000000000..b598b592d
--- /dev/null
+++ b/apps/demo/.env.example
@@ -0,0 +1 @@
+# The Worker reads .dev.vars. Copy .dev.vars.example to .dev.vars.
diff --git a/apps/demo/README.md b/apps/demo/README.md
new file mode 100644
index 000000000..7ac0dec7b
--- /dev/null
+++ b/apps/demo/README.md
@@ -0,0 +1,44 @@
+# HackKit demo
+
+A public demonstration of HackKit Core, the Next integration, package UI, and Teams. Vinext runs the application on Cloudflare Workers. Turso stores application data over HTTP. A private R2 binding stores uploads. The app owns its pages and configuration and does not import either template or E2E support.
+
+## Credentials and local setup
+
+Copy `.dev.vars.example` to **`apps/demo/.dev.vars`**. Fill in `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, and a random `BETTER_AUTH_SECRET` of at least 32 characters. Keep `BETTER_AUTH_URL=http://localhost:3001` for local development. This ignored file is the only local credential file used by the demo. Never point it at a real event database.
+
+From the repository root:
+
+```sh
+corepack pnpm install
+corepack pnpm exec turbo run build --filter=@hackkit/demo^...
+corepack pnpm --filter @hackkit/demo plugin:sync
+corepack pnpm --filter @hackkit/demo db:migrate
+corepack pnpm --filter @hackkit/demo db:demo
+corepack pnpm dev:demo
+```
+
+Open [localhost:3001](http://localhost:3001). Local development and preview run in Workerd. They use the configured Turso database and an emulated local R2 bucket. Local uploads persist under `.wrangler`. Database setup is an explicit Node command; requests never migrate or seed the database.
+
+`db:demo` seeds fictional accounts, a team, and four schedule entries only when there are no existing users. Existing users are retained. Owner assignment is an explicit demo bootstrap operation. Neither template assigns owners automatically. `db:reset` is disabled for the shared demo database. Other database commands follow the template interface.
+
+`corepack pnpm build:demo` compiles MikroORM entity functions ahead of time and builds the Worker. It needs no database connection or credentials. `corepack pnpm --filter @hackkit/demo start` previews that production build on port 3001.
+
+## Cloudflare deployment
+
+The committed configuration targets **ACM UTSA**, account `cdc7630e53b468f56a4453af71a58ade`, Worker `hackkit-demo`, and private R2 bucket `hackkit-demo-uploads`. Verify Wrangler's account with `corepack pnpm --filter @hackkit/demo exec wrangler whoami`.
+
+Set `vars.BETTER_AUTH_URL` in `wrangler.jsonc` to the exact public origin. Default: [hackkit-demo.acm-utsa.workers.dev](https://hackkit-demo.acm-utsa.workers.dev). Set these Worker secrets through Wrangler's interactive `secret put` command or the Cloudflare dashboard:
+
+- `TURSO_DATABASE_URL`
+- `TURSO_AUTH_TOKEN`
+- `BETTER_AUTH_SECRET`
+
+For example, `corepack pnpm --filter @hackkit/demo exec wrangler secret put TURSO_AUTH_TOKEN` prompts for the token without putting it in command history. Do not upload the local `BETTER_AUTH_URL` as a secret; the deployed origin belongs in Wrangler configuration. R2 uses the `DEMO_UPLOADS` binding and requires no access keys or public bucket URL.
+
+Run `corepack pnpm deploy:demo` after building workspace libraries. Run `corepack pnpm --filter @hackkit/demo deploy:check` after a build to inspect the deployment without publishing.
+
+The hosted demo shares persistent Turso data and R2 uploads between visitors. Participant and organizer accounts are public. **Try registration** creates a new fictional account. Use fictional data only. Email delivery, OAuth, and Discord synchronization are disabled. This public demonstration is unsuitable for real participant records.
+
+## Verification
+
+Run `DEMO_TEST_URL=http://localhost:3001 corepack pnpm --filter @hackkit/demo test:smoke` against a running production preview. The checks cover role navigation, onboarding, permissions, and authenticated R2 uploads. CI uses a disposable local libSQL service and emulated R2; it never receives deployed database credentials.
diff --git a/apps/web/app/admin/check-in/page.tsx b/apps/demo/app/admin/check-in/page.tsx
similarity index 100%
rename from apps/web/app/admin/check-in/page.tsx
rename to apps/demo/app/admin/check-in/page.tsx
diff --git a/apps/web/app/admin/events/[id]/edit/page.tsx b/apps/demo/app/admin/events/[id]/edit/page.tsx
similarity index 100%
rename from apps/web/app/admin/events/[id]/edit/page.tsx
rename to apps/demo/app/admin/events/[id]/edit/page.tsx
diff --git a/apps/web/app/admin/events/new/page.tsx b/apps/demo/app/admin/events/new/page.tsx
similarity index 100%
rename from apps/web/app/admin/events/new/page.tsx
rename to apps/demo/app/admin/events/new/page.tsx
diff --git a/apps/web/app/admin/events/page.tsx b/apps/demo/app/admin/events/page.tsx
similarity index 100%
rename from apps/web/app/admin/events/page.tsx
rename to apps/demo/app/admin/events/page.tsx
diff --git a/apps/web/app/admin/layout.tsx b/apps/demo/app/admin/layout.tsx
similarity index 100%
rename from apps/web/app/admin/layout.tsx
rename to apps/demo/app/admin/layout.tsx
diff --git a/apps/web/app/admin/page.tsx b/apps/demo/app/admin/page.tsx
similarity index 100%
rename from apps/web/app/admin/page.tsx
rename to apps/demo/app/admin/page.tsx
diff --git a/apps/web/app/admin/roles/page.tsx b/apps/demo/app/admin/roles/page.tsx
similarity index 100%
rename from apps/web/app/admin/roles/page.tsx
rename to apps/demo/app/admin/roles/page.tsx
diff --git a/apps/web/app/admin/scanner/[eventId]/page.tsx b/apps/demo/app/admin/scanner/[eventId]/page.tsx
similarity index 100%
rename from apps/web/app/admin/scanner/[eventId]/page.tsx
rename to apps/demo/app/admin/scanner/[eventId]/page.tsx
diff --git a/apps/web/app/admin/settings/page.tsx b/apps/demo/app/admin/settings/page.tsx
similarity index 100%
rename from apps/web/app/admin/settings/page.tsx
rename to apps/demo/app/admin/settings/page.tsx
diff --git a/apps/web/app/admin/users/[authId]/page.tsx b/apps/demo/app/admin/users/[authId]/page.tsx
similarity index 100%
rename from apps/web/app/admin/users/[authId]/page.tsx
rename to apps/demo/app/admin/users/[authId]/page.tsx
diff --git a/apps/web/app/admin/users/page.tsx b/apps/demo/app/admin/users/page.tsx
similarity index 100%
rename from apps/web/app/admin/users/page.tsx
rename to apps/demo/app/admin/users/page.tsx
diff --git a/apps/web/app/api/admin/export/route.ts b/apps/demo/app/api/admin/export/route.ts
similarity index 100%
rename from apps/web/app/api/admin/export/route.ts
rename to apps/demo/app/api/admin/export/route.ts
diff --git a/apps/web/app/api/auth/[...all]/route.ts b/apps/demo/app/api/auth/[...all]/route.ts
similarity index 100%
rename from apps/web/app/api/auth/[...all]/route.ts
rename to apps/demo/app/api/auth/[...all]/route.ts
diff --git a/apps/demo/app/api/files/register/route.ts b/apps/demo/app/api/files/register/route.ts
new file mode 100644
index 000000000..97cf1586d
--- /dev/null
+++ b/apps/demo/app/api/files/register/route.ts
@@ -0,0 +1,23 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+import { getAuthSession } from "@/lib/auth";
+import { createUploadTarget } from "@/lib/blob";
+
+const upload = z.object({
+ location: z.enum(["resumes", "profile-photos"]),
+ fileName: z.string().min(1).max(255),
+});
+export async function POST(request: Request) {
+ const session = await getAuthSession();
+ if (!session)
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ const input = upload.safeParse(await request.json().catch(() => null));
+ if (!input.success)
+ return NextResponse.json(
+ { error: "Choose a resume or profile photo and a file name." },
+ { status: 400 },
+ );
+ return NextResponse.json(
+ createUploadTarget({ ...input.data, ownerId: session.user.id }),
+ );
+}
diff --git a/apps/demo/app/api/files/upload/route.ts b/apps/demo/app/api/files/upload/route.ts
new file mode 100644
index 000000000..6744e2b94
--- /dev/null
+++ b/apps/demo/app/api/files/upload/route.ts
@@ -0,0 +1,36 @@
+import { NextResponse } from "next/server";
+import { getAuthSession } from "@/lib/auth";
+import { canUploadKey, getBlobStorage, readUploadBody } from "@/lib/blob";
+
+export async function PUT(request: Request) {
+ const session = await getAuthSession();
+ if (!session)
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ const key = new URL(request.url).searchParams.get("key");
+ if (!key || !canUploadKey(key, session.user.id))
+ return NextResponse.json(
+ { error: "Unsupported upload key." },
+ { status: 403 },
+ );
+ try {
+ const body = await readUploadBody(request);
+ const contentType =
+ request.headers.get("content-type") ?? "application/octet-stream";
+ if (
+ key.startsWith("profile-photos/") &&
+ !["image/png", "image/jpeg", "image/webp"].includes(contentType)
+ )
+ return NextResponse.json(
+ { error: "Choose a PNG, JPEG, or WebP image." },
+ { status: 400 },
+ );
+ await getBlobStorage().put(key, body, {
+ httpMetadata: { contentType },
+ });
+ return new NextResponse(null, { status: 204 });
+ } catch (error) {
+ if (error instanceof RangeError)
+ return NextResponse.json({ error: error.message }, { status: 413 });
+ throw error;
+ }
+}
diff --git a/apps/demo/app/api/files/view/route.ts b/apps/demo/app/api/files/view/route.ts
new file mode 100644
index 000000000..0e40822cd
--- /dev/null
+++ b/apps/demo/app/api/files/view/route.ts
@@ -0,0 +1,54 @@
+import { NextResponse } from "next/server";
+import { CorePermission } from "@hackkit/core";
+import { getAuthSession } from "@/lib/auth";
+import { getBlobStorage } from "@/lib/blob";
+import { getRuntime } from "@/lib/runtime";
+
+function storedFileReferenceForKey(key: string): string {
+ return `/api/files/view?key=${encodeURIComponent(key)}`;
+}
+
+async function canViewFile(key: string): Promise {
+ if (key.startsWith("profile-photos/")) return true;
+
+ const session = await getAuthSession();
+ if (!session) return false;
+
+ const runtime = await getRuntime();
+ const currentUser = await runtime.getCurrentUser();
+ const hacker = await runtime.hackkit.hackers.getHacker(currentUser.id);
+ if (hacker?.resumeUrl === storedFileReferenceForKey(key)) return true;
+
+ return runtime.hackkit.accessControl.hasPermission(
+ currentUser.id,
+ CorePermission.HackersView,
+ );
+}
+
+export async function GET(request: Request): Promise {
+ const url = new URL(request.url);
+ const key = url.searchParams.get("key");
+ if (!key) {
+ return NextResponse.json({ error: "key is required" }, { status: 400 });
+ }
+ if (!key.startsWith("resumes/") && !key.startsWith("profile-photos/")) {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
+ if (!(await canViewFile(key))) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const storage = getBlobStorage();
+
+ const result = await storage.get(key);
+ if (!result)
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ const headers = new Headers();
+ result.writeHttpMetadata(headers);
+ headers.set("Cache-Control", "private, no-store");
+ headers.set("X-Content-Type-Options", "nosniff");
+ headers.set("Content-Security-Policy", "sandbox");
+ if (!key.startsWith("profile-photos/"))
+ headers.set("Content-Disposition", "attachment");
+ return new NextResponse(result.body, { headers });
+}
diff --git a/apps/web/app/api/hackkit/[...path]/route.ts b/apps/demo/app/api/hackkit/[...path]/route.ts
similarity index 100%
rename from apps/web/app/api/hackkit/[...path]/route.ts
rename to apps/demo/app/api/hackkit/[...path]/route.ts
diff --git a/apps/web/app/app-bar.tsx b/apps/demo/app/app-bar.tsx
similarity index 100%
rename from apps/web/app/app-bar.tsx
rename to apps/demo/app/app-bar.tsx
diff --git a/apps/web/app/contact/page.tsx b/apps/demo/app/contact/page.tsx
similarity index 100%
rename from apps/web/app/contact/page.tsx
rename to apps/demo/app/contact/page.tsx
diff --git a/apps/web/app/dashboard/layout.tsx b/apps/demo/app/dashboard/layout.tsx
similarity index 100%
rename from apps/web/app/dashboard/layout.tsx
rename to apps/demo/app/dashboard/layout.tsx
diff --git a/apps/web/app/dashboard/page.tsx b/apps/demo/app/dashboard/page.tsx
similarity index 100%
rename from apps/web/app/dashboard/page.tsx
rename to apps/demo/app/dashboard/page.tsx
diff --git a/apps/demo/app/demo-actions.tsx b/apps/demo/app/demo-actions.tsx
new file mode 100644
index 000000000..d47f3abe8
--- /dev/null
+++ b/apps/demo/app/demo-actions.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { authClient } from "@/lib/auth-client";
+import { demoAccounts, demoPassword } from "@/lib/demo-accounts";
+
+export function DemoActions() {
+ const [ready, setReady] = useState(false);
+ const [pending, setPending] = useState();
+ useEffect(() => setReady(true), []);
+ const [error, setError] = useState();
+ async function enter(role: "participant" | "organizer" | "newcomer") {
+ setPending(role);
+ setError(undefined);
+ try {
+ await authClient.signOut();
+ const result =
+ role === "newcomer"
+ ? await authClient.signUp.email({
+ name: "Demo participant",
+ email: `demo-${crypto.randomUUID()}@example.com`,
+ password: demoPassword,
+ })
+ : await authClient.signIn.email({
+ email: demoAccounts[role].email,
+ password: demoPassword,
+ });
+ if (result.error)
+ throw new Error(result.error.message ?? "Unable to open demo.");
+ window.location.assign(
+ role === "organizer" ? "/admin" : "/dashboard",
+ );
+ } catch (cause) {
+ setError(
+ cause instanceof Error ? cause.message : "Unable to open demo.",
+ );
+ setPending(undefined);
+ }
+ }
+ return (
+ <>
+
+ {(
+ [
+ [
+ "participant",
+ "Explore as a participant",
+ "Open a registered hacker’s dashboard, create a team, and manage your event pass.",
+ ],
+ [
+ "organizer",
+ "Explore as an organizer",
+ "Review applications, manage roles and settings, and edit the event schedule.",
+ ],
+ [
+ "newcomer",
+ "Try registration",
+ "Create a fresh fictional account and walk through the onboarding forms.",
+ ],
+ ] as const
+ ).map(([role, title, description]) => (
+
+ ))}
+
+ {error && (
+
+ {error}
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/app/globals.css b/apps/demo/app/globals.css
similarity index 100%
rename from apps/web/app/globals.css
rename to apps/demo/app/globals.css
diff --git a/apps/web/app/help/page.tsx b/apps/demo/app/help/page.tsx
similarity index 100%
rename from apps/web/app/help/page.tsx
rename to apps/demo/app/help/page.tsx
diff --git a/apps/web/app/i/approval/layout.tsx b/apps/demo/app/i/approval/layout.tsx
similarity index 100%
rename from apps/web/app/i/approval/layout.tsx
rename to apps/demo/app/i/approval/layout.tsx
diff --git a/apps/web/app/i/approval/page.tsx b/apps/demo/app/i/approval/page.tsx
similarity index 100%
rename from apps/web/app/i/approval/page.tsx
rename to apps/demo/app/i/approval/page.tsx
diff --git a/apps/web/app/invites/layout.tsx b/apps/demo/app/invites/layout.tsx
similarity index 100%
rename from apps/web/app/invites/layout.tsx
rename to apps/demo/app/invites/layout.tsx
diff --git a/apps/web/app/invites/page.tsx b/apps/demo/app/invites/page.tsx
similarity index 100%
rename from apps/web/app/invites/page.tsx
rename to apps/demo/app/invites/page.tsx
diff --git a/apps/demo/app/layout.tsx b/apps/demo/app/layout.tsx
new file mode 100644
index 000000000..23ab39b78
--- /dev/null
+++ b/apps/demo/app/layout.tsx
@@ -0,0 +1,45 @@
+import type * as React from "react";
+import type { Metadata } from "next";
+import { PublicSiteFooter } from "@hackkit/ui";
+import {
+ getPublicThemeStyle,
+ publicSiteConfig,
+} from "@/lib/public-site-config";
+import "./globals.css";
+import { AppBar } from "./app-bar";
+import { Providers } from "./providers";
+
+export const metadata: Metadata = {
+ title: publicSiteConfig.brand.name,
+ description: publicSiteConfig.brand.description,
+ robots: { index: false, follow: false },
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+ Build a hackathon portal around your event. Start with
+ registration and profiles, then add teams, event operations, and
+ the integrations you need.
+
+
+
+ Get started
+
+
+ Explore the demo
+
+
+
+ {[
+ {
+ icon: Terminal,
+ title: "Runnable templates",
+ description:
+ "Choose minimal Core registration or the full example. Run either locally with the workspace.",
+ href: "/docs/templates",
+ },
+ {
+ icon: Puzzle,
+ title: "A shared Core, optional plugins",
+ description:
+ "Keep domain rules and permissions in Core. Configure features and pages in your application.",
+ href: "/docs/architecture",
+ },
+ {
+ icon: BookOpen,
+ title: "Operate with confidence",
+ description:
+ "Use reviewed migrations, explicit roles, and integration checks against the actual templates.",
+ href: "/docs/testing",
+ },
+ ].map(({ icon: Icon, title, description, href }) => (
+
+
+