Deployment: 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-opspreserves basketball-api's hands-off status. The only basketball-api touch is one SQL migration (thewestside_ops_readerrole). 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, notfunnel. This is a window into production data. Defense in depth demands a network-layer gate before any application-layer auth. Withtailscale.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_readerrole has explicitGRANT SELECTon 14 tables and nothing else.oauth_tokens(which contains Gmail access and refresh tokens in JSONB),password_reset_tokens, andoutboxare unreachable at the DB layer. This means even a Streamlit bug that tried to execute arbitrary SQL would getpermission deniedon 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.pyfile 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 thewestside_ops_readerrole 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 toharbor.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 |
Related
story-westside-ops-spreadsheet-access— the user story this architecture servesarch-domain-westside-ops— which tables are exposed and how they group into pagesarch-dataflow-westside-ops— runtime flow: Marcus → Tailscale → Keycloak → Streamlit → Postgresnocodb-basketball-api-scoping(archived) — the schema audit + GRANT allowlist + NocoDB evaluation that informed this architecturefeedback_basketball_hands_off,feedback_enterprise_no_workarounds,feedback_never_stomp_archbox— relevant memoriesboard-westside-ops— tickets serving this architecture