lbzfai.com — evolution plan
S3 Worker-proxy path deferred per ADR-005 (Tailscale-only Phase I). Worker-proxy + Cloudflare Tunnel design preserved in
docs/design/60-parking/cloudflare-tunnel-from-jetson.md.
Status: draft Author: Agent C (frontend bucket, round 1) Reviewers: Andrew Kent, Sophia Mann, Agent B (deploy infra dependencies), Agent E (tenancy posture); round-2 critique agent Last updated: 2026-05-10
Context
Section titled “Context”lbzfai.com is currently one Astro page (src/pages/index.astro) with the entire app inline in a <script> block: Auth0 SPA client initialization, login flow, signed-in render. Astro is configured for output: 'static' and deploys to Cloudflare Workers with Static Assets (NOT Pages — see ~/lbzfai/CLAUDE.md). The Auth0 SPA Client ID and Domain are hardcoded with env fallback. public/.assetsignore exists and is mandatory.
Per Andrew’s 2026-05-10 directive — “all the nested pages users will see on the platform” — the site needs to grow from one page into ~12 pages by July 2026. The growth must not break:
- Static output (no SSR introduction without explicit decision)
- The hardcoded Auth0 fallback deploy story (until replaced with documented build vars)
- The Cloudflare Workers with Static Assets path (different surface from Pages)
- The auto-deploy on push to
main
Anchor artifacts:
~/lbzfai/CLAUDE.md(entire file is the architectural baseline)~/lbzfai/src/pages/index.astro(the inline-app file we are splitting up)~/lbzfai/astro.config.mjs~/lbzfai/package.json- This bucket’s
information-architecture.md(the target sitemap) - This bucket’s
auth0-roles-and-access.md(the role enforcement we are wiring)
- A staged plan to go from “one inline-app file” to “real Astro project structure with a dozen pages” without breaking deploys.
- Concrete file/folder layout for the next 2 months.
- An honest decision tree for “when do we add SSR / framework / dynamic routes” — and the conditions under which we don’t.
- Preserve the deploy story (Cloudflare Workers + static assets, push-to-main auto-deploys,
output: 'static'). - Preserve the Auth0 hardcoded-fallback pattern unless explicitly replacing.
Non-goals
Section titled “Non-goals”- Replacing Astro with another framework. Astro is fine; the question is how we use it.
- Server-side rendering. Off the table for Phase I; possibly for Phase II.
- A separate dashboard SPA framework (React/Vue/Svelte). Astro components + per-page islands are sufficient.
- A new monorepo. The
~/lbzfairepo stays one repo. - Hosting Mariana’s dashboard data API in this repo. The API lives on the Jetson (Phase I) or in a Cloudflare Worker proxying the Jetson (Phase II, possibly).
Proposed approach
Section titled “Proposed approach”Three stages
Section titled “Three stages”| Stage | When | What changes | Risk |
|---|---|---|---|
| S1. Split inline app into Astro structure | now → 2026-05-22 | Inline <script> becomes a real src/lib/auth/auth0.ts module; new pages added under src/pages/app/*; layouts factored. Still output: 'static'. Still Cloudflare Workers + Static Assets. Still hardcoded Auth0 fallback. | Low. Pure refactor + add pages. |
| S2. Auth0 role enforcement + Spanish-first landing | 2026-05-22 → 2026-06-15 | /app dispatcher; per-role routes (/app/ejecutivo, /app/ingeniero, /app/admin, /app/cuenta); landing page Spanish-ified per localization-es-co.md. | Low–medium. New Auth0 Action; per-page role checks; Spanish translation. |
| S3. | Phase II | Worker-proxy via outbound tunnel is deferred to Phase II. Phase I Mariana view shows aggregates against mocked data with a clear “Datos de demostración” label; for real numbers she installs Tailscale and reaches the Jetson dashboard directly. Design preserved in docs/design/60-parking/cloudflare-tunnel-from-jetson.md. | n/a — deferred. |
S1 and S2 are the Phase I scope. Mariana’s /app/ejecutivo ships against mocked aggregates in Phase I (per ADR-005); the real proxy path resumes in Phase II.
Target file layout (post-S2)
Section titled “Target file layout (post-S2)”src/├── layouts/│ ├── Layout.astro # existing│ ├── PublicLayout.astro # marketing landing wrapper│ ├── AppLayout.astro # authenticated app shell (Spanish header, role-aware nav)│ └── AdminLayout.astro # admin shell (English)├── pages/│ ├── index.astro # marketing landing — refactored, no inline auth│ ├── soporte.astro # static support page│ ├── app/│ │ ├── index.astro # /app dispatcher (role-aware redirect)│ │ ├── ejecutivo/│ │ │ ├── index.astro # Mariana home (4 module tiles + trend)│ │ │ ├── historico.astro│ │ │ └── modulo/[name].astro # per-module drill-down (dynamic route)│ │ ├── ingeniero/│ │ │ ├── index.astro # Ronald lbzfai.com landing ("Open dashboard")│ │ │ ├── abrir-jetson.astro # smart-redirect to Jetson IP│ │ │ └── exportar.astro # Excel export trigger UI│ │ ├── investigacion/│ │ │ └── index.astro # ITBA aggregate view│ │ ├── admin/│ │ │ └── index.astro # admin home (English)│ │ └── cuenta/│ │ └── index.astro # account page (unassigned-role landing)│ └── 404.astro├── components/│ ├── ModuleTile.astro # used by /app/ejecutivo/index│ ├── TrendChart.astro # used by /app/ejecutivo/index│ ├── RoleGate.astro # client-side role check wrapper│ ├── Header.astro│ └── Footer.astro├── lib/│ ├── auth/│ │ ├── auth0.ts # SPA client init (replaces inline <script>)│ │ └── roles.ts # hasRole / hasAnyRole helpers (see auth0-roles-and-access.md)│ ├── i18n/│ │ └── es-CO.ts # canonical strings (see localization-es-co.md)│ └── api/│ └── jetson.ts # (S3) typed fetch client for the Cloudflare→Jetson proxy└── env.d.tssrc/pages/index.astro’s inline <script> block gets extracted into src/lib/auth/auth0.ts exporting a singleton client. The page imports it and is left with markup + a tiny <script> that calls getAuth0().
How role-gating works on static pages
Section titled “How role-gating works on static pages”Astro output: 'static' means every page is pre-rendered HTML at build time. Per-page role enforcement happens client-side, in a small inline script at the top of each protected page:
---import AppLayout from '@/layouts/AppLayout.astro';---<AppLayout title="Resumen ejecutivo"> <div id="root" hidden> <!-- the page --> </div>
<script> import { getAuth0 } from '@/lib/auth/auth0'; import { hasAnyRole } from '@/lib/auth/roles';
const auth0 = await getAuth0(); if (!await auth0.isAuthenticated()) { auth0.loginWithRedirect(); } else if (!await hasAnyRole(auth0, 'executive', 'admin')) { location.href = '/app'; } else { document.getElementById('root')!.hidden = false; } </script></AppLayout>This works because:
- Public-by-design Auth0 SPA values — we are not protecting anything at the HTML-static layer beyond a redirect.
- Real data access happens through API calls that themselves validate the Auth0 access token server-side (on the Jetson, or in the Cloudflare Worker proxy).
- A user who removes the role check in DevTools sees an empty
<div id="root">. The API behind it still rejects them. This is acceptable.
Why we don’t need SSR for Phase I
Section titled “Why we don’t need SSR for Phase I”Common reasons to add SSR:
| Reason | Phase I answer |
|---|---|
| SEO for protected pages | N/A — protected pages aren’t indexed. |
| Personalized HTML | We do this client-side after Auth0 returns. |
| Don’t show flash of unauthenticated content | A 200ms hidden-while-checking state is acceptable. Use hidden attribute. |
| Server-side env var injection | The hardcoded Auth0 SPA fallback handles this; build-time import.meta.env covers the rest. |
| API routes | The API lives on the Jetson or in a Cloudflare Worker, not in Astro. |
If a real need for SSR emerges in Phase II (e.g., Mariana’s email summary), we add a Cloudflare Worker route handler — not Astro SSR.
Cloudflare deploy story (preserving what works)
Section titled “Cloudflare deploy story (preserving what works)”Per CLAUDE.md:
- Deployment is Cloudflare Workers with Static Assets, not Pages.
public/.assetsignoremust exist.- Cloudflare’s build VM runs
astro add cloudflare, mutatingastro.config.mjsonly inside the build. Do not commit that mutation locally. - Build-time env vars: Cloudflare dashboard → Settings → Builds → Build variables. Different surface from Workers runtime “Variables and Secrets”.
S1 and S2 add HTML pages and a couple of .ts modules — they don’t change the deploy story. Phase I stays in output: 'static' end-to-end.
If/when the parked Worker-proxy thaws in Phase II, two deploy options re-open:
- Option A: add a Worker script alongside the static assets —
src/worker.tsconsumed by Cloudflare Workers with Static Assets, with custom routes. - Option B: migrate to a hybrid Astro adapter
@astrojs/cloudflarewithoutput: 'hybrid', allowing per-routeprerender = falsefor API endpoints.
Phase I does not make this call. The decision lives in the parked design.
Tenancy posture
Section titled “Tenancy posture”Andrew’s “all the nested pages users will see on the platform” could be parsed two ways:
- Narrow: all the pages of the LBZF dashboard. Single tenant.
- Expansive: all the pages of a platform that hosts factory dashboards for multiple manufacturers.
Phase I and Phase II are single-tenant (LBZF + an ITBA replica). Phase III may not exist commercially. But the URL and role conventions in this doc are tenant-friendly:
- Roles namespaced under
https://lbzfai.com/roles(auth0-roles-and-access.md) → tenant can be added ashttps://lbzfai.com/tenant. - User metadata field
lbzfai.tenant = "lbzf"set in Phase I; ITBA twin users getlbzfai.tenant = "itba-bsas". - Page graph (
/app/ejecutivo/modulo/<name>) is per-module today, but the dynamic slug pattern would extend to/app/t/<tenant>/ejecutivo/modulo/<name>with surgical additions.
Decision: do not migrate to Auth0 Organizations or multi-tenant routes in Phase I. Plant the seeds (namespaced claims, tenant metadata) so a future migration is mechanical. Detailed plan owned by Agent E.
Migration trigger checklist
Section titled “Migration trigger checklist”These are the events that would change this evolution plan:
- A second customer signs. → Migrate to Auth0 Organizations; introduce
/app/t/<tenant>/routes; consider SSR for tenant-aware pages. - Mariana refuses Tailscale and demands live video. → Cloudflare Tunnel + Auth0-on-Jetson; possibly SSR for the streaming page.
- ITBA’s twin must be on the same lbzfai.com (vs separate hosting). → Multi-tenant routes; consider sub-domain
itba.lbzfai.comvs path-based. - Astro deploys break inside Cloudflare Workers with Static Assets → Pages migration (already non-trivial; document the rollback path).
- Bundle size of
@auth0/auth0-spa-jsbecomes a problem on Mariana’s slow phone → Lazy-load Auth0 only on/app/*pages.
What stays the same forever (until explicitly replaced)
Section titled “What stays the same forever (until explicitly replaced)”- Auth0 hardcoded fallback for SPA Client ID + Domain. Do not remove unless build vars are wired through Cloudflare.
output: 'static'inastro.config.mjs(committed). Cloudflare’s build VM may mutate it; do not commit that mutation.public/.assetsignorecommitted and non-empty enough to satisfy Cloudflare’s deploy.- es-CO for user-facing surfaces; English for admin.
- Push-to-main auto-deploys via Cloudflare’s native GitHub integration; no GitHub Actions workflow in the repo.
Concrete S1 task list (now → 2026-05-22)
Section titled “Concrete S1 task list (now → 2026-05-22)”- Extract
src/pages/index.astro’s<script>intosrc/lib/auth/auth0.tsexporting a memoizedgetAuth0(). - Create
src/lib/auth/roles.tswithRole,getRoles,hasRole,hasAnyRole. - Create
src/lib/i18n/es-CO.tswith the canonical Spanish strings fromlocalization-es-co.md. - Create
src/layouts/AppLayout.astro(authenticated shell). - Create the page stubs (placeholders, no real data):
src/pages/app/index.astro(dispatcher)src/pages/app/ejecutivo/index.astrosrc/pages/app/ingeniero/index.astrosrc/pages/app/investigacion/index.astrosrc/pages/app/admin/index.astrosrc/pages/app/cuenta/index.astro
- Refactor
src/pages/index.astro’s sign-in button to redirect post-login to/app. - Confirm
npm run buildstill emits a singledist/that Cloudflare can deploy. No new dependencies inpackage.jsonfor S1.
Concrete S2 task list (2026-05-22 → 2026-06-15)
Section titled “Concrete S2 task list (2026-05-22 → 2026-06-15)”- In Auth0, create the five roles and deploy the Login Action (see
auth0-roles-and-access.md). - Add the per-page role guard pattern to every
/app/*page. - Translate
src/pages/index.astroto Spanish (preserve current visual design). - Add Mariana’s home view (
/app/ejecutivo/index.astro) against mocked JSON data. - Add Ronald’s lbzfai.com landing (
/app/ingeniero/index.astro) with the “Abrir tablero (Jetson + Tailscale)” link. - Add the dispatcher logic in
/app/index.astro.
S3 deferred to Phase II per ADR-005
Section titled “S3 deferred to Phase II per ADR-005”The Worker-proxy + outbound-tunnel design previously slotted here is parked. See docs/design/60-parking/cloudflare-tunnel-from-jetson.md. Phase I’s /app/ejecutivo renders against mocked aggregates with a “Datos de demostración” banner; real data requires Tailscale-direct Jetson access (or a Phase II thaw).
Alternatives considered
Section titled “Alternatives considered”- Rewrite in Next.js (or another React framework). Rejected — Astro is working; the team has zero benefit and high migration cost.
- Move the whole app to the Jetson dashboard and let lbzfai.com stay as a landing page only. This is essentially “Option A” from
information-architecture.md. Rejected for Mariana reasons. - Server-side render every page from a Cloudflare Worker. Rejected for Phase I — too much new infra, too little user benefit. Re-consider in Phase II if Mariana’s email summary needs server rendering.
- Use Astro Server Islands. Considered. Defers nicely if/when we need partial dynamic content. Probably the right move for Phase II if S3’s proxy approach feels heavy.
- Add Tailwind / a UI kit. Plausible. Not blocking. Defer to whoever does the visual-design pass.
Open questions
Section titled “Open questions”- CLOSED via ADR-005: When does S3 (Cloudflare → Jetson proxy) ship? Phase II. Mariana’s Phase I view shows mocked aggregates. See
60-parking/cloudflare-tunnel-from-jetson.md. - OPEN: Move build vars into Cloudflare’s Build Variables and remove the hardcoded Auth0 fallback — when, if ever? Owner: Sophia, target: low priority; the hardcoded fallback is intentional per
CLAUDE.md. - OPEN: Astro adapter migration trigger. When (if ever) do we move from
output: 'static'to'hybrid'? Default: never, until the parked Worker-proxy thaws. - OPEN: Multi-tenant pivot — is this real? Owner: Agent E + Andrew, target: end of Phase I (July 2026).
- OPEN: Per-page bundle splitting. When
@auth0/auth0-spa-jsis imported in every page, do we lazy-load on protected pages only? Owner: Sophia, target: 2026-07-01 (profile first).
Cross-bucket dependencies
Section titled “Cross-bucket dependencies”- From backend/integration (Agent B):
- Worker-proxy contract is parked (Phase II); see
60-parking/cloudflare-tunnel-from-jetson.md. - Excel export endpoint that Ronald’s
/app/ingeniero/exportarproxies to (Phase I: link points to Jetson Tailscale URL). - Mariana’s mocked-data shape (Phase I); real per-module aggregate API is Phase II.
- Worker-proxy contract is parked (Phase II); see
- From hardware/ML (Agent A, D): none directly. Indirectly: the inference pipeline emits the events that S3 consumes.
- From business/cross-cutting (Agent E):
- Tenancy posture — does multi-tenant happen? When? With which customer?
- Privacy policy + legal review surface (
/soporte, footer link on every page). - Confirmation that the Auth0 SPA hardcoded fallback’s “public-by-design” framing is acceptable for an enterprise audience (it is, but record the decision).
What’s weak in this doc
Section titled “What’s weak in this doc”- S3 is deferred to Phase II per ADR-005. Mariana’s Phase I view shows mocked aggregates clearly labeled as demo data. If she finds that insufficient, ADR-005 re-opens and the parked Cloudflare-Tunnel design becomes Phase I scope.
- No bundle-size budget. We don’t know what
@auth0/auth0-spa-jsweighs in the current build, nor what the budget should be on Mariana’s phone. The doc proposes lazy-loading but does not measure. - No CI/CD changes addressed. The “push-to-main auto-deploys” story is stated as immutable, but as the app grows we may want a staging deploy, preview branches, or a real test step before deploy. None of that is here.
- The role-gate-in-client-script pattern is convenient but is not a security boundary. This is acknowledged in §“How role-gating works”, but the doc does not enumerate what would happen if Auth0 itself was misconfigured (a
researchuser gainingadminclaim). We rely entirely on Auth0’s correct token issuance. - Multi-tenant readiness is asserted, not tested. “Tenant metadata, namespaced claims, dynamic slugs” sound right, but until we actually stand up a second tenant in dev, we don’t know what breaks.
- No documented rollback if Cloudflare Workers with Static Assets deprecates or changes. The deploy story is brittle to Cloudflare product changes.
Rollout
Section titled “Rollout”- Argentina demo (2026-05-13): S1 mostly complete; placeholder
/app/*pages live; sign-in still works. - 2026-05-22: S1 done. Real folder structure, no inline
<script>block, marketing landing in Spanish. - 2026-06-15: S2 done. Role-gated routes, Mariana home on mock data, Ronald lbzfai.com landing.
- 2026-07-01: Phase I go-live. S3 explicitly deferred to Phase II per ADR-005.
- Phase II (Aug 2026+): thaw
60-parking/cloudflare-tunnel-from-jetson.mdif needed; consider Astro Server Islands, lazy-load Auth0 per-route, add a staging deploy, evaluate hybrid output. - Phase III (2027+): revisit multi-tenancy; possibly migrate to Auth0 Organizations.
Appendix / references
Section titled “Appendix / references”~/lbzfai/CLAUDE.md~/lbzfai/src/pages/index.astro~/lbzfai/astro.config.mjs~/lbzfai/package.json- Cloudflare Workers Static Assets docs
- Astro Cloudflare adapter
- This bucket’s
information-architecture.md - This bucket’s
auth0-roles-and-access.md - This bucket’s
localization-es-co.md