Deployment: westside-ops

arch-deployment-westside-ops Architecture

architecture active westside-ops

Deployment: westside-ops

Diagram

Components

Component Purpose Notes
<code>westside-ops</code> namespace Isolation for the Streamlit workload Created by pal-e-services <code>var.services["westside-ops"]</code> for_each
Streamlit pod Runs <code>streamlit_admin.py</code>, serves the 9 grid pages Single replica, image pulled from Harbor, env vars from SOPS secret
Tailscale Ingress Exposes the pod at <code>westside-ops.tail5b443a.ts.net</code> — <strong>PRIVATE, not a funnel</strong> Annotation: <code>tailscale.com/expose: "true"</code>. Only reachable from tailnet members.
ArgoCD Application Declarative deploy; syncs from the <code>westside-ops</code> Forgejo repo's <code>k8s/</code> directory Created by pal-e-services. Image Updater watches Harbor for new SHAs.
Harbor project <code>westside-ops</code> Container image registry for the Streamlit app Created by pal-e-services. CI robot pushes from Woodpecker. Pull robot creates <code>harbor-creds</code> secret in the namespace.
Keycloak client <code>westside-ops</code> OIDC authentication Added to the <strong>existing</strong> <code>westside</code> realm. Marcus's existing account + a new <code>westside-ops-user</code> role.
<code>westside_ops_reader</code> Postgres role Defense-in-depth: restricted DB user Created inside basketball-api's Postgres pod via a one-shot SQL script. Explicit GRANT allowlist on 14 tables. Cannot SELECT <code>oauth_tokens</code>, <code>password_reset_tokens</code>, <code>outbox</code>.
SOPS secrets <code>secrets.enc.yaml</code> OIDC client secret, DB connection URL, Streamlit cookie secret Age-encrypted, committed to the repo. Same pattern as <code>harbor-creds.enc.yaml</code> in existing overlays.
basketball-api Postgres (unchanged) Source of truth for all Westside data Per-service Postgres pod in <code>basketball-api</code> namespace. westside-ops reads cross-namespace via <code>postgres.basketball-api.svc.cluster.local:5432</code>. <strong>Hands-off</strong> beyond the one role-creation SQL.
Woodpecker pipeline Builds the Streamlit image on every push to main, pushes to Harbor Single repo <code>westside-ops</code> has its own <code>.woodpecker.yaml</code>. Kaniko build, standard harbor_username/password secrets.

Key Decisions

  • Workload, not platform capability. westside-ops is a westside-specific operator tool — it serves one tenant's data via one instance. That makes it a workload, not a platform capability, and workloads follow the kustomize-overlay pattern (like basketball-api, westsidekingsandqueens, mcd-tracker) rather than the Helm-release-in-tofu pattern (like Harbor, Keycloak, CNPG operator). Zero pal-e-platform changes.
  • Sibling repo, not folded into basketball-api. A separate Forgejo repo forgejo_admin/westside-ops preserves basketball-api's hands-off status. The only basketball-api touch is one SQL migration (the westside_ops_reader role). Upgrades, new Streamlit pages, and Marcus-specific customizations all happen in westside-ops without touching basketball-api's code, tests, or deploy pipeline.
  • Tailscale expose, not funnel. This is a window into production data. Defense in depth demands a network-layer gate before any application-layer auth. With tailscale.com/expose: "true", the login page is only reachable from tailnet members. Marcus installs Tailscale on his phone (one-time, 3 minutes), Lucas adds him to the tailnet, and Keycloak becomes the second gate instead of the only one. Public funnel was explicitly rejected — the cost of reversing later is too high if data leaks, and Marcus installing Tailscale is trivially cheap.
  • Defense-in-depth at the Postgres role level, not the application level. The westside_ops_reader role has explicit GRANT SELECT on 14 tables and nothing else. oauth_tokens (which contains Gmail access and refresh tokens in JSONB), password_reset_tokens, and outbox are unreachable at the DB layer. This means even a Streamlit bug that tried to execute arbitrary SQL would get permission denied on the forbidden tables — security does not depend on the UI being correct.
  • No metadata database — Streamlit is stateless by design. Unlike NocoDB (which needs its own Postgres for views, user mappings, API tokens), Streamlit's state lives in the streamlit_admin.py file itself (queries and page definitions, versioned in git) and per-request session state (in-memory). If the pod restarts, Marcus re-logs in via Keycloak; nothing else is lost. This eliminates the "CNPG Tier-1 cluster for metadata" requirement and the associated backup ceremony entirely.
  • Read-only for v1. Streamlit supports editable grids via st.data_editor(df, disabled=False), and the westside_ops_reader role grants only SELECT. Flipping to read-write later is one SQL grant (GRANT INSERT, UPDATE ON players TO westside_ops_reader) plus one Python arg flip — but v1 ships read-only because "Marcus can't accidentally corrupt production" is a more valuable property than "Marcus can edit from his phone."
  • Harbor-mirrored Streamlit base image. The container image is built from python:3.12-slim + pip install streamlit + the app source, then pushed to harbor.tail5b443a.ts.net/westside-ops/app:{SHA}. Every workload image in the cluster comes from Harbor — no direct Docker Hub pulls — so westside-ops inherits vulnerability scanning and supply-chain provenance automatically.
  • Soft launch via direct DM, not integrated links. v1 ships at a standalone URL not linked from westside-app's existing admin. Lucas DMs Marcus the URL privately. If Marcus likes it, phase 2 can consider adding a "Data Grid" card on westside-app's admin dashboard (option B from the rollout plan) or migrating parts of the existing admin into Streamlit pages (option C). Keeping the v1 rollout decoupled means zero risk to Marcus's current workflow.

What changes in each repo

Repo Change Ticket
<code>pal-e-platform</code> None (zero)
<code>pal-e-services</code> New <code>var.services["westside-ops"]</code> entry + <code>keycloak_clients["westside-ops"]</code> services-entry
<code>pal-e-deployments</code> None — westside-ops manages its own <code>k8s/</code> inside its own repo, ArgoCD points at it directly via <code>source_repo</code> override
<code>westside-ops</code> (new) Full repo bootstrap: <code>streamlit_admin.py</code>, Dockerfile, requirements.txt, .woodpecker.yaml, k8s/ overlay repo-bootstrap, streamlit-app, woodpecker-pipeline, k8s-overlay
<code>basketball-api</code> (hands-off) One SQL migration applied via <code>kubectl exec</code>: create <code>westside_ops_reader</code> role + GRANT allowlist postgres-role
  • story-westside-ops-spreadsheet-access — the user story this architecture serves
  • arch-domain-westside-ops — which tables are exposed and how they group into pages
  • arch-dataflow-westside-ops — runtime flow: Marcus → Tailscale → Keycloak → Streamlit → Postgres
  • nocodb-basketball-api-scoping (archived) — the schema audit + GRANT allowlist + NocoDB evaluation that informed this architecture
  • feedback_basketball_hands_off, feedback_enterprise_no_workarounds, feedback_never_stomp_archbox — relevant memories
  • board-westside-ops — tickets serving this architecture