minutework 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/assets/claude-local/skills/README.md +2 -0
  2. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -1
  3. package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
  4. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -3
  5. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
  6. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
  7. package/assets/templates/next-tenant-app/README.md +26 -138
  8. package/assets/templates/next-tenant-app/package.json +1 -0
  9. package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
  10. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
  11. package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
  12. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
  13. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
  14. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
  15. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
  16. package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
  17. package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
  18. package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
  19. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
  20. package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
  21. package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
  22. package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
  23. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
  24. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
  25. package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
  26. package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
  27. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
  28. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
  29. package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
  30. package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
  31. package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
  32. package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
  33. package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
  34. package/assets/templates/next-tenant-app/template.json +3 -3
  35. package/assets/templates/next-tenant-app/template.schema.json +1 -0
  36. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
  37. package/package.json +2 -2
  38. package/vendor/workspace-mcp/types.d.ts +4 -0
  39. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
  40. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
  41. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
  42. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
  43. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
  44. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
  45. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
  46. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
  47. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
  48. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
  49. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
  50. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
  51. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
  52. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
  53. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
  54. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
  55. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
  56. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
  57. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
  58. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
  59. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
  60. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
  61. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
  62. package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
  63. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
  64. package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
  65. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
  66. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
  67. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
  68. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
  69. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
  70. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
  71. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
  72. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
  73. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
  74. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
  75. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
  76. package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
  77. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
  78. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
  79. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
  80. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
  81. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
  82. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
  83. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
  84. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
  85. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
  86. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
  87. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
  88. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
  89. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
  90. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
  91. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
@@ -15,6 +15,8 @@ receive:
15
15
 
16
16
  - Builder should compose shared substrate before generating bespoke tenant code
17
17
  - `tenant-app` is the combined public plus private web starter
18
+ - `tenant-app` auth/data uses `@minutework/web-auth` and thin `src/mw/`
19
+ substrate, not per-app auth/gateway BFF routes
18
20
  - `mw.core.site` is a runtime baseline capability
19
21
  - tenant public authoring is runtime/CMS-backed through `mw.core.site`
20
22
  - Vuilder-owned public sites use `vuilder-public-site` and public-dj CMS
@@ -16,6 +16,19 @@ An `app pack` is the shipped product unit.
16
16
  contract boundaries.
17
17
  - Start with declarative schema/manifests and add code surfaces only when needed.
18
18
  - Use `tenant-app` for the combined public-site plus private-app web surface.
19
+ - `tenant-app` auth/data must stay on the frozen `@minutework/web-auth`
20
+ contract. Wire it through the thin `src/mw/` substrate only; do not add
21
+ per-app `src/app/api/auth/*`, `src/app/api/gateway/*`, platform-session BFF
22
+ layers, platform credential login screens, operator-console links, or
23
+ runtime-command demos.
24
+ - Browser calls from `tenant-app` should use `mw.query(...)` and
25
+ `mw.action(..., { idempotencyKey })`; actions require a non-empty
26
+ idempotency key. The runtime surface must be declared on the manifest with
27
+ `webCustomerExposed` / `web_customer_exposed`, and V1 customer roles are
28
+ `customer` only.
29
+ - The authenticated `tenant-app` principal is `tenant_customer`. Do not model
30
+ the generated customer app as an operator/platform-session app, and do not
31
+ persist bearer/session/runtime tokens in browser JavaScript.
19
32
  - Use `sidecar` for internal APIs, workers, integrations, or Python-heavy compute.
20
33
  - For member-facing collaboration and operator workflows, check shell fit first
21
34
  before proposing standalone `tenant-app` routes or dashboards.
@@ -77,7 +90,7 @@ An `app pack` is the shipped product unit.
77
90
  `SCHEMA_KEY_UPSERT_REGISTRY` may ship in the active manifest.
78
91
  - For `mw.core.site.form`, do not author `form_key` or `app_pack_ref` inside
79
92
  `data`; the install pipe injects both. Keep `surface_key == form_key` for
80
- public intake BFFs.
93
+ public intake gateway handling.
81
94
  - For `mw.runtime.agent`, use `logical_key` as the runtime `Agent.slug`.
82
95
  Author stable agent fields in `data` such as `name`, `system_instruction`,
83
96
  `enabled_tools`, `trigger_intents`, `channels_enabled`, and
@@ -35,6 +35,14 @@ guest-continuity threads, public submission routing, or claim-after-auth flows.
35
35
  - Post-auth continuation stays on the existing `handoff_key` and flows through
36
36
  `apply_post_auth_access_policy()`. Do not invent automatic membership upgrades
37
37
  or a parallel claim token model.
38
+ - Tenant web auth `claimRef` is an opaque platform-signed handoff claim token,
39
+ not a raw `PublicHandoff.id` or `core:public_handoff:*` ref. A `tenant-app`
40
+ may pass that token through `@minutework/web-auth` signup, but platform must
41
+ store it pending verification and link only after the authenticating email is
42
+ verified, the signed handoff is bound to that verified identity, and
43
+ `apply_post_auth_access_policy()` succeeds.
44
+ - Do not create customer identity claims during unverified signup. Anonymous or
45
+ unbound handoffs must not be auto-linked.
38
46
 
39
47
  ## Privacy And Prompt Semantics
40
48
 
@@ -65,9 +73,9 @@ guest-continuity threads, public submission routing, or claim-after-auth flows.
65
73
  source that can drive later `action:` or `flow:` execution.
66
74
  - Do not gate public intake render on a hardcoded client-side token
67
75
  allowlist. The handoff token is a platform-issued, server-validated
68
- payload; the workspace UI should pass it through to the BFF and let the
69
- platform decide validity. A client-side allowlist drifts from the real
70
- token registry and silently breaks once real tokens are minted.
76
+ payload; the workspace UI should pass it through to the hosted gateway/auth
77
+ path and let the platform decide validity. A client-side allowlist drifts
78
+ from the real token registry and silently breaks once real tokens are minted.
71
79
 
72
80
  ## Vuilder Vertical Onboarding
73
81
 
@@ -22,13 +22,20 @@ the monorepo or a live tenant runtime.
22
22
  - `tenant-app`, optional `sidecar`, and the optional `mobile` starter are
23
23
  implementation surfaces inside the tenant product, not the whole architecture.
24
24
  - `tenant-app` is the combined public plus private web starter.
25
+ - In `tenant-app`, keep MinuteWork web auth/data access inside the thin
26
+ `src/mw/` substrate and `@minutework/web-auth`. Product UI may call that
27
+ substrate, but it must not recreate platform-session BFF routes, platform
28
+ credential login, or browser-exposed platform/runtime tokens.
29
+ - `tenant-app` runtime data access goes through `mw.query(...)` and
30
+ `mw.action(..., { idempotencyKey })` against manifest-declared
31
+ `webCustomerExposed` / `web_customer_exposed` customer surfaces.
25
32
  - A mobile / native client (Expo, React Native, native iOS/Android) is a
26
33
  standalone-client exception authored in the `mobile` starter, not in
27
34
  `tenant-app` or `sidecar`. It is a direct platform API client that consumes
28
35
  the platform native-token API (`/api/v1/native/...`) directly with a
29
- platform-issued native session token, rather than the `tenant-app` BFF cookie
30
- session. The mobile starter remains npm-owned standalone app code and should
31
- not be added to the generated root `pnpm-workspace.yaml`.
36
+ platform-issued native session token, rather than the `tenant-app` web SDK /
37
+ hosted-cookie session. The mobile starter remains npm-owned standalone app
38
+ code and should not be added to the generated root `pnpm-workspace.yaml`.
32
39
  - If the `mobile` starter is enabled (`starters.mobile.enabled`), read
33
40
  `standalone-mobile-client/SKILL.md` before proposing the mobile surface.
34
41
  - `vuilder-public-site` is a Vuilder-owned public-site authoring workspace for
@@ -20,18 +20,21 @@ flows, or the default MinuteWork site model.
20
20
  runtime-canonical through `mw.core.site` and installed app-pack data.
21
21
  - `tenant-app` is the combined web starter for public routes plus the private
22
22
  `/app` workspace.
23
+ - Do not add a server-only public content token adapter to `tenant-app`.
24
+ Public content should come from hosted published releases/snapshots or
25
+ gateway-approved public-content routes over the MinuteWork substrate, not
26
+ from a baked-in platform token in the generated app.
23
27
  - Author public content against runtime/CMS records, not a fixed in-repo site
24
28
  starter.
25
29
  - For contract-first intake pipeline decisions, also read
26
30
  `contract-first-public-intake/SKILL.md`.
27
31
  - Use `published-web` and hosted public-release flows for anonymous delivery by
28
32
  default.
29
- - `MW_PUBLIC_CONTENT_SOURCE` selects the public content adapter mode.
30
- - `MW_CONTENT_API_TOKEN`, `MW_PUBLIC_SITE_PROPERTY_KEY`, and
31
- `MW_PUBLIC_SITE_ENV` are required only for `MW_PUBLIC_CONTENT_SOURCE=minutework_cms`.
32
- - `MW_STATIC_PUBLIC_CONTENT_PATH` is used for `MW_PUBLIC_CONTENT_SOURCE=static_json`.
33
- - `MW_PUBLIC_SITE_ENV=preview` is for preview or draft-safe reads.
34
- - `MW_PUBLIC_SITE_ENV=live` is for publication-safe reads only.
33
+ - Legacy starter variables such as `MW_PUBLIC_CONTENT_SOURCE`,
34
+ `MW_CONTENT_API_TOKEN`, `MW_PUBLIC_SITE_PROPERTY_KEY`,
35
+ `MW_STATIC_PUBLIC_CONTENT_PATH`, and `MW_PUBLIC_SITE_ENV` describe old
36
+ adapter modes. Do not introduce them into new `tenant-app` work unless a
37
+ compatibility task explicitly targets that legacy adapter.
35
38
  - Anonymous live traffic should prefer published snapshots instead of direct
36
39
  runtime reads on every request.
37
40
  - `runtime_local_sidecar` is an explicit exception path for live serving, not
@@ -80,9 +80,9 @@ The native token flow mirrors the existing CLI developer-token flow
80
80
  freshly returned pair. Re-run the browser-assisted authorize step only when
81
81
  the refresh token itself is invalid or revoked.
82
82
 
83
- The browser/BFF cookie path belongs to `tenant-app` and is **web-only**. The
84
- mobile app does not ride the `tenant-app` cookie jar, and it does not read or
85
- forward CSRF -- native uses the bearer token directly against the platform.
83
+ The `tenant-app` web SDK / hosted-cookie path is **web-only**. The mobile app
84
+ does not ride the `tenant-app` cookie jar, and it does not read or forward
85
+ CSRF -- native uses the bearer token directly against the platform.
86
86
 
87
87
  ### Token binding (what is and isn't enforced)
88
88
 
@@ -100,7 +100,8 @@ boundary.
100
100
  - Do not mint a JWT inside the app or stand up a local user table / token issuer.
101
101
  - Do not expose access or refresh tokens to web pages or generic React state;
102
102
  keep them in `expo-secure-store`.
103
- - Do not route the mobile app through the `tenant-app` BFF cookie session.
103
+ - Do not route the mobile app through the `tenant-app` web SDK / hosted-cookie
104
+ session.
104
105
  - Do not read or forward CSRF from native; bearer-token auth does not use it.
105
106
  - Do not put native code inside `tenant-app` or `sidecar`.
106
107
 
@@ -3,157 +3,45 @@
3
3
  This directory is the canonical combined Next.js web starter for Builder.
4
4
 
5
5
  In the runtime Builder flow, this bundle materializes into
6
- `BuilderWorkspace.sandbox_root/app/`.
6
+ `BuilderWorkspace.sandbox_root/app/`. In the external CLI scaffold flow, the
7
+ same starter is copied into `tenant-app/`.
7
8
 
8
- In the external CLI scaffold flow, the same starter is copied into
9
- `tenant-app/`.
10
-
11
- On managed tenant VMs, `BuilderWorkspace.sandbox_root` normally lives under
12
- `/srv/minutework/workspaces/<workspace_id>/`, so the Builder-edited app copy is
13
- typically `/srv/minutework/workspaces/<workspace_id>/app/`.
14
-
15
- `apps/mw-next-client` is still the repo-side seed used to harden and evolve this scaffold. It is not the runtime template location.
9
+ `apps/mw-next-client` may still seed visual conventions, but this template is
10
+ no longer a platform-session BFF app.
16
11
 
17
12
  ## Auth profile
18
13
 
19
- This template stays fixed to the `platform_session_bff` profile:
20
-
21
- - browsers use this BFF only (cookie session, server-owned CSRF); native clients
22
- (e.g. a mobile app) instead use a platform-issued native session token
23
- directly against `/api/v1/native/...` -- see the `mobile` starter /
24
- `standalone-mobile-client` skill
25
- - upstream platform `sessionid` and `csrftoken` stay server-owned
26
- - unsafe upstream requests bootstrap and forward CSRF through the BFF
27
- - there is no browser-to-runtime direct access
28
- - there is no local JWT system, local user table, or parallel auth stack
29
-
30
- ## Runtime requirements
31
-
32
- - Node.js `20.9.0` or newer
33
- - `pnpm@9.0.0`
14
+ This template uses the `tenant_web_auth_sdk` profile:
34
15
 
35
- ## Direct web hosts
16
+ - browser auth and manifest API calls go through `@minutework/web-auth`
17
+ - the browser stores no bearer token, runtime key, or platform credential
18
+ - SDK calls use same-origin `/_mw` routes with `credentials: "include"`
19
+ - CSRF is handled by the SDK using the non-secret CSRF cookie
20
+ - runtime query/action calls are authorized as `principal_kind=tenant_customer`
36
21
 
37
- When this starter lives inside an external CLI workspace with an optional
38
- `sidecar`, deploy direct web hosts against `tenant-app` only.
39
-
40
- - Root `pnpm install` stays Node-only and does not auto-install sidecar Python deps.
41
- - If you need the sidecar locally, run `pnpm run install:sidecar` from the workspace root or `cd sidecar && poetry install`.
42
- - For Vercel, set the project Root Directory to `tenant-app`.
43
-
44
- Those host-specific instructions apply to the CLI scaffold layout, not the
45
- runtime Builder sandbox layout under `app/`.
22
+ Only `src/mw/` is MinuteWork substrate. Keep product UI and product logic in
23
+ `src/app`, `src/features`, and developer-owned modules.
46
24
 
47
25
  ## Default route shape
48
26
 
49
- The template ships one combined app with two explicit route surfaces:
50
-
51
- - public routes at the root: `/`, `/pricing`, `/docs`, `/blog`, `/login`
52
- - authenticated routes under `/app`
53
- - optional runtime example under `/app/examples/runtime-commands`
54
-
55
- Public marketing, docs, and blog routes render through a swappable public-content adapter seam. The authenticated shell keeps tenant context controls, password flows, and the platform-session BFF boundary.
56
-
57
- ## Public content adapter
58
-
59
- The public content seam lives under `src/lib/content`.
60
-
61
- The default built-in adapter is `MinuteWorkPublicSiteContentAdapter`. It calls the
62
- MinuteWork gateway with a server-only content token and reads the narrow
63
- `mw.core.site` public-site snapshot contract.
64
-
65
- `mw.core.site` is treated as a runtime baseline capability. This starter
66
- composes against that baseline rather than trying to install it locally.
67
-
68
- The gateway response carries an explicit boundary:
69
-
70
- - `environment=preview` must declare `source_boundary=runtime_preview`
71
- - `environment=live` may declare `source_boundary=published_live` for hosted releases or `source_boundary=runtime_live` for runtime-served sidecar releases
72
-
73
- That keeps preview free to use runtime-backed draft reads while keeping live
74
- publication-safe for hosted delivery while still allowing an explicit
75
- runtime-served public lane when the release is activated through
76
- `runtime_local_sidecar`.
77
-
78
- To replace the built-ins for a tenant-specific CMS or content API, implement the hook in `src/lib/content/custom-adapter.ts`.
79
-
80
- ## Example gating
27
+ - public routes at `/`, `/pricing`, `/docs`, and `/blog`
28
+ - customer auth at `/login`
29
+ - authenticated workspace at `/app`
30
+ - SDK manifest demo at `/app/demo`
81
31
 
82
- `MW_ENABLE_RUNTIME_COMMAND_EXAMPLE=false` by default.
83
-
84
- When disabled:
85
-
86
- - `/app/examples/runtime-commands` returns `404`
87
- - the example gateway API routes return `404`
88
- - the example navigation entry is omitted
32
+ The template intentionally does not include `src/lib/platform/*`,
33
+ `src/app/api/auth/*`, `src/app/api/gateway/*`, an operator-console link, a
34
+ runtime-command demo, or a server-only public-content token adapter.
89
35
 
90
36
  ## Environment
91
37
 
92
- Copy `.env.example` to `.env.local` when running the template directly. The
93
- example file uses `MW_PUBLIC_CONTENT_SOURCE=none` so the private `/app` surface
94
- can boot without MinuteWork CMS credentials; set `MW_PUBLIC_CONTENT_SOURCE` to
95
- `minutework_cms` and fill the CMS values below when enabling the public site.
96
-
97
- - `MW_PLATFORM_BASE_URL` is required
98
- - `MW_PUBLIC_CONTENT_SOURCE` defaults to `none` when omitted; supported values are `minutework_cms`, `custom`, `static_json`, and `none`
99
- - `MW_CONTENT_API_TOKEN` is required only for `minutework_cms` and must stay server-only
100
- - `MW_TEMPLATE_APP_NAME` defaults to `MinuteWork Combined Starter`
101
- - `MW_PUBLIC_BASE_URL` is required for public content sources and should match the deployed public origin; it may be omitted when `MW_PUBLIC_CONTENT_SOURCE=none`
102
- - `MW_PUBLIC_SITE_PROPERTY_KEY` is required only for `minutework_cms` and selects the `PublishedWebProperty` backing the site
103
- - `MW_PUBLIC_SITE_ENV` defaults to `preview`
104
- - `MW_STATIC_PUBLIC_CONTENT_PATH` defaults to `content/public-site.json` and is used when `MW_PUBLIC_CONTENT_SOURCE=static_json`
105
- - `MW_ENABLE_RUNTIME_COMMAND_EXAMPLE` defaults to `false`
106
-
107
- ## SEO baseline
108
-
109
- The public route surface ships with:
110
-
111
- - canonical URLs derived from `MW_PUBLIC_BASE_URL`
112
- - `robots.ts` and `sitemap.ts`
113
- - Open Graph metadata for marketing and article/detail pages
114
- - server-rendered public pages by default, with no tenant/session reads during render
115
- - docs/blog static params generated from the same public-site snapshot used during render
116
- - graceful empty states for fresh properties with no authored or no published content
117
-
118
- Private routes under `/app` are marked `noindex`.
119
-
120
- ## Generated artifact policy
121
-
122
- Checked-in generated artifacts are limited to:
123
-
124
- - `next-env.d.ts`
125
- - `src/design-system/tokens/manifest.ts`
126
- - `src/design-system/tokens/manifest.json`
127
- - `pnpm-lock.yaml`
128
-
129
- Generated build outputs such as `.next/`, `storybook-static/`, `test-results/`, and `tools/design-system/__screenshots__/` are not canonical template contents.
38
+ - `NEXT_PUBLIC_MW_APP_ID` selects the manifest app id used by the SDK and
39
+ defaults to `tenant.app`
40
+ - `MW_TEMPLATE_APP_NAME` controls the display name and defaults to `Tenant App`
41
+ - `MW_PUBLIC_BASE_URL` is optional and only affects metadata routes
130
42
 
131
43
  ## Validation
132
44
 
133
- Use `pnpm validate` from this directory. It runs:
134
-
135
- 1. `template:validate`
136
- 2. `template:route-contract`
137
- 3. `next typegen`
138
- 4. `design-system:tokens`
139
- 5. `design-system:check`
140
- 6. `typecheck`
141
- 7. `lint`
142
- 8. `test`
143
- 9. `build:validate`
144
- 10. `build-storybook:validate`
145
-
146
- `template:validate` validates `template.json` against the bundled `template.schema.json`, so the template remains self-validating after Builder materializes it into a workspace sandbox.
147
-
148
- When the CLI scaffolds this starter into `tenant-app/`, the same validation
149
- contract still applies to that copied workspace surface. When Runtime Builder
150
- materializes it into `app/`, the same validation contract applies there too.
151
-
152
- The validation-only build steps inject fixture values for the required
153
- production env vars and run against a local public-site fixture server, so the
154
- template can prove its build contract in-repo without weakening the real
155
- `pnpm build` requirement.
156
-
157
- `template:route-contract` verifies that the combined public/private route tree, metadata routes, and content-adapter entrypoints remain present in the template.
158
-
159
- Inside the repo, the shared schema at `runtime/builder/templates/template.schema.json` is authoritative. The bundled `template.schema.json` inside this template must stay byte-equivalent, and `template:validate` fails if the two drift.
45
+ Use `pnpm validate` from this directory. It runs template validation, route
46
+ contract validation, typegen, design-system checks, typecheck, tests, and build
47
+ validation.
@@ -27,6 +27,7 @@
27
27
  "design-system:visual": "playwright test -c tools/design-system/playwright.config.mjs"
28
28
  },
29
29
  "dependencies": {
30
+ "@minutework/web-auth": "^0.1.0",
30
31
  "@radix-ui/react-slot": "^1.2.4",
31
32
  "class-variance-authority": "^0.7.1",
32
33
  "clsx": "^2.1.1",
@@ -0,0 +1,15 @@
1
+ import { ManifestDemo } from "@/features/demo/components/manifest-demo";
2
+
3
+ export const metadata = {
4
+ title: "Demo",
5
+ };
6
+
7
+ export default function DemoPage() {
8
+ return (
9
+ <main className="min-h-screen bg-background text-foreground">
10
+ <div className="mx-auto flex min-h-screen max-w-5xl flex-col gap-6 px-6 py-8">
11
+ <ManifestDemo />
12
+ </div>
13
+ </main>
14
+ );
15
+ }
@@ -1,8 +1,6 @@
1
1
  import type { Metadata } from "next";
2
2
  import type { ReactNode } from "react";
3
3
 
4
- import { resolveAuthenticatedSession } from "@/lib/platform/auth.server";
5
-
6
4
  export const metadata: Metadata = {
7
5
  robots: {
8
6
  index: false,
@@ -10,11 +8,10 @@ export const metadata: Metadata = {
10
8
  },
11
9
  };
12
10
 
13
- export default async function AuthenticatedAppLayout({
11
+ export default function AuthenticatedAppLayout({
14
12
  children,
15
13
  }: {
16
14
  children: ReactNode;
17
15
  }) {
18
- await resolveAuthenticatedSession();
19
16
  return children;
20
17
  }
@@ -1,24 +1,9 @@
1
1
  import { PrivateAppShell } from "@/features/shell/components/private-app-shell";
2
- import { resolveAuthenticatedSession } from "@/lib/platform/auth.server";
3
- import { platformAuthEndpoints } from "@/lib/platform/endpoints.server";
4
- import { env } from "@/lib/platform/env.server";
5
2
 
6
3
  export const metadata = {
7
4
  title: "Workspace",
8
- description:
9
- "Authenticated workspace surface for the combined MinuteWork starter.",
10
5
  };
11
6
 
12
- export default async function AppHomePage() {
13
- const session = await resolveAuthenticatedSession();
14
-
15
- return (
16
- <PrivateAppShell
17
- initialSession={session}
18
- appName={env.MW_TEMPLATE_APP_NAME}
19
- operatorConsoleHref={platformAuthEndpoints.operatorConsole}
20
- runtimeCommandExampleEnabled={env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE}
21
- view="dashboard"
22
- />
23
- );
7
+ export default function AppHomePage() {
8
+ return <PrivateAppShell appName={process.env.MW_TEMPLATE_APP_NAME || "Tenant App"} />;
24
9
  }
@@ -1,73 +1,15 @@
1
- import { notFound } from "next/navigation";
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
2
 
3
- import { ContentArticle } from "@/features/public-shell/components/content-article";
4
- import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
5
- import { getEntry, getSiteConfig, listEntries } from "@/lib/content/adapter.server";
6
- import { appRoutes } from "@/lib/app-routes";
7
- import {
8
- buildDisabledPublicMetadata,
9
- buildPublicMetadata,
10
- isPublicContentDisabled,
11
- } from "@/lib/public-site";
12
-
13
- type BlogArticlePageProps = {
14
- params: Promise<{ slug: string }>;
3
+ export const metadata = {
4
+ title: "Blog",
15
5
  };
16
6
 
17
- export const dynamicParams = false;
18
-
19
- export async function generateStaticParams() {
20
- if (isPublicContentDisabled()) {
21
- return [];
22
- }
23
-
24
- return (await listEntries("blog")).map((entry) => ({
25
- slug: entry.slug[0],
26
- }));
27
- }
28
-
29
- export async function generateMetadata({ params }: BlogArticlePageProps) {
30
- if (isPublicContentDisabled()) {
31
- return buildDisabledPublicMetadata();
32
- }
33
-
34
- const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
35
- const entry = await getEntry("blog", [slug]);
36
-
37
- if (!entry) {
38
- return buildPublicMetadata({
39
- title: siteConfig.collections.blog.title,
40
- description: siteConfig.collections.blog.description,
41
- path: appRoutes.blogIndex,
42
- siteName: siteConfig.siteName,
43
- });
44
- }
45
-
46
- return buildPublicMetadata({
47
- title: entry.seo.title,
48
- description: entry.seo.description,
49
- path: appRoutes.blogPost(entry.slug),
50
- siteName: siteConfig.siteName,
51
- type: "article",
52
- publishedTime: entry.publishedAt,
53
- });
54
- }
55
-
56
- export default async function BlogArticlePage({ params }: BlogArticlePageProps) {
57
- if (isPublicContentDisabled()) {
58
- notFound();
59
- }
60
-
61
- const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
62
- const entry = await getEntry("blog", [slug]);
63
-
64
- if (!entry) {
65
- notFound();
66
- }
67
-
7
+ export default function BlogPostPage() {
68
8
  return (
69
- <PublicSiteShell activeHref={appRoutes.blogIndex} siteConfig={siteConfig}>
70
- <ContentArticle entry={entry} />
71
- </PublicSiteShell>
9
+ <StaticPublicPage
10
+ eyebrow="Blog"
11
+ title="Update"
12
+ body="A customer-facing update."
13
+ />
72
14
  );
73
15
  }
@@ -1,51 +1,15 @@
1
- import { notFound } from "next/navigation";
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
2
 
3
- import { ContentCollection } from "@/features/public-shell/components/content-collection";
4
- import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
5
- import { getSiteConfig, listEntries } from "@/lib/content/adapter.server";
6
- import { appRoutes } from "@/lib/app-routes";
7
- import {
8
- buildDisabledPublicMetadata,
9
- buildPublicMetadata,
10
- isPublicContentDisabled,
11
- } from "@/lib/public-site";
12
-
13
- export async function generateMetadata() {
14
- if (isPublicContentDisabled()) {
15
- return buildDisabledPublicMetadata();
16
- }
17
-
18
- const siteConfig = await getSiteConfig();
19
- const collection = siteConfig.collections.blog;
20
-
21
- return buildPublicMetadata({
22
- title: collection.title,
23
- description: collection.description,
24
- path: appRoutes.blogIndex,
25
- siteName: siteConfig.siteName,
26
- });
27
- }
28
-
29
- export default async function BlogIndexPage() {
30
- if (isPublicContentDisabled()) {
31
- notFound();
32
- }
33
-
34
- const [siteConfig, entries] = await Promise.all([
35
- getSiteConfig(),
36
- listEntries("blog"),
37
- ]);
38
- const collection = siteConfig.collections.blog;
3
+ export const metadata = {
4
+ title: "Blog",
5
+ };
39
6
 
7
+ export default function BlogIndexPage() {
40
8
  return (
41
- <PublicSiteShell activeHref={appRoutes.blogIndex} siteConfig={siteConfig}>
42
- <ContentCollection
43
- description={collection.description}
44
- entries={entries}
45
- eyebrow={collection.eyebrow}
46
- kind="blog"
47
- title={collection.title}
48
- />
49
- </PublicSiteShell>
9
+ <StaticPublicPage
10
+ eyebrow="Blog"
11
+ title="Updates"
12
+ body="News, releases, and customer notes."
13
+ />
50
14
  );
51
15
  }
@@ -1,71 +1,15 @@
1
- import { notFound } from "next/navigation";
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
2
 
3
- import { ContentArticle } from "@/features/public-shell/components/content-article";
4
- import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
5
- import { getEntry, getSiteConfig, listEntries } from "@/lib/content/adapter.server";
6
- import { appRoutes } from "@/lib/app-routes";
7
- import {
8
- buildDisabledPublicMetadata,
9
- buildPublicMetadata,
10
- isPublicContentDisabled,
11
- } from "@/lib/public-site";
12
-
13
- type DocsArticlePageProps = {
14
- params: Promise<{ slug: string[] }>;
3
+ export const metadata = {
4
+ title: "Docs",
15
5
  };
16
6
 
17
- export const dynamicParams = false;
18
-
19
- export async function generateStaticParams() {
20
- if (isPublicContentDisabled()) {
21
- return [];
22
- }
23
-
24
- return (await listEntries("docs")).map((entry) => ({
25
- slug: entry.slug,
26
- }));
27
- }
28
-
29
- export async function generateMetadata({ params }: DocsArticlePageProps) {
30
- if (isPublicContentDisabled()) {
31
- return buildDisabledPublicMetadata();
32
- }
33
-
34
- const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
35
- const entry = await getEntry("docs", slug);
36
-
37
- if (!entry) {
38
- return buildPublicMetadata({
39
- title: siteConfig.collections.docs.title,
40
- description: siteConfig.collections.docs.description,
41
- path: appRoutes.docsIndex,
42
- siteName: siteConfig.siteName,
43
- });
44
- }
45
-
46
- return buildPublicMetadata({
47
- title: entry.seo.title,
48
- description: entry.seo.description,
49
- path: appRoutes.docsPage(entry.slug),
50
- siteName: siteConfig.siteName,
51
- });
52
- }
53
-
54
- export default async function DocsArticlePage({ params }: DocsArticlePageProps) {
55
- if (isPublicContentDisabled()) {
56
- notFound();
57
- }
58
-
59
- const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
60
- const entry = await getEntry("docs", slug);
61
-
62
- if (!entry) {
63
- notFound();
64
- }
65
-
7
+ export default function DocsPage() {
66
8
  return (
67
- <PublicSiteShell activeHref={appRoutes.docsIndex} siteConfig={siteConfig}>
68
- <ContentArticle entry={entry} />
69
- </PublicSiteShell>
9
+ <StaticPublicPage
10
+ eyebrow="Docs"
11
+ title="Documentation page"
12
+ body="Helpful context for customer workflows."
13
+ />
70
14
  );
71
15
  }