Skip to content

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

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.
  • 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 ~/lbzfai repo 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).
StageWhenWhat changesRisk
S1. Split inline app into Astro structurenow → 2026-05-22Inline <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 landing2026-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. Cloudflare → Jetson data proxy for Mariana — PARKED (ADR-005)Phase IIWorker-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.

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.ts

src/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().

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:

src/pages/app/ejecutivo/index.astro
---
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:

  1. Public-by-design Auth0 SPA values — we are not protecting anything at the HTML-static layer beyond a redirect.
  2. 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).
  3. 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.

Common reasons to add SSR:

ReasonPhase I answer
SEO for protected pagesN/A — protected pages aren’t indexed.
Personalized HTMLWe do this client-side after Auth0 returns.
Don’t show flash of unauthenticated contentA 200ms hidden-while-checking state is acceptable. Use hidden attribute.
Server-side env var injectionThe hardcoded Auth0 SPA fallback handles this; build-time import.meta.env covers the rest.
API routesThe 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/.assetsignore must exist.
  • Cloudflare’s build VM runs astro add cloudflare, mutating astro.config.mjs only 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.ts consumed by Cloudflare Workers with Static Assets, with custom routes.
  • Option B: migrate to a hybrid Astro adapter @astrojs/cloudflare with output: 'hybrid', allowing per-route prerender = false for API endpoints.

Phase I does not make this call. The decision lives in the parked design.

Andrew’s “all the nested pages users will see on the platform” could be parsed two ways:

  1. Narrow: all the pages of the LBZF dashboard. Single tenant.
  2. 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 as https://lbzfai.com/tenant.
  • User metadata field lbzfai.tenant = "lbzf" set in Phase I; ITBA twin users get lbzfai.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.

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.com vs 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-js becomes 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' in astro.config.mjs (committed). Cloudflare’s build VM may mutate it; do not commit that mutation.
  • public/.assetsignore committed 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)”
  1. Extract src/pages/index.astro’s <script> into src/lib/auth/auth0.ts exporting a memoized getAuth0().
  2. Create src/lib/auth/roles.ts with Role, getRoles, hasRole, hasAnyRole.
  3. Create src/lib/i18n/es-CO.ts with the canonical Spanish strings from localization-es-co.md.
  4. Create src/layouts/AppLayout.astro (authenticated shell).
  5. Create the page stubs (placeholders, no real data):
    • src/pages/app/index.astro (dispatcher)
    • src/pages/app/ejecutivo/index.astro
    • src/pages/app/ingeniero/index.astro
    • src/pages/app/investigacion/index.astro
    • src/pages/app/admin/index.astro
    • src/pages/app/cuenta/index.astro
  6. Refactor src/pages/index.astro’s sign-in button to redirect post-login to /app.
  7. Confirm npm run build still emits a single dist/ that Cloudflare can deploy. No new dependencies in package.json for S1.

Concrete S2 task list (2026-05-22 → 2026-06-15)

Section titled “Concrete S2 task list (2026-05-22 → 2026-06-15)”
  1. In Auth0, create the five roles and deploy the Login Action (see auth0-roles-and-access.md).
  2. Add the per-page role guard pattern to every /app/* page.
  3. Translate src/pages/index.astro to Spanish (preserve current visual design).
  4. Add Mariana’s home view (/app/ejecutivo/index.astro) against mocked JSON data.
  5. Add Ronald’s lbzfai.com landing (/app/ingeniero/index.astro) with the “Abrir tablero (Jetson + Tailscale)” link.
  6. Add the dispatcher logic in /app/index.astro.

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).

  • 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.
  • 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-js is imported in every page, do we lazy-load on protected pages only? Owner: Sophia, target: 2026-07-01 (profile first).
  • 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/exportar proxies to (Phase I: link points to Jetson Tailscale URL).
    • Mariana’s mocked-data shape (Phase I); real per-module aggregate API is Phase II.
  • 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).
  1. 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.
  2. No bundle-size budget. We don’t know what @auth0/auth0-spa-js weighs in the current build, nor what the budget should be on Mariana’s phone. The doc proposes lazy-loading but does not measure.
  3. 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.
  4. 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 research user gaining admin claim). We rely entirely on Auth0’s correct token issuance.
  5. 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.
  6. No documented rollback if Cloudflare Workers with Static Assets deprecates or changes. The deploy story is brittle to Cloudflare product changes.
  • 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.md if 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.