Project not found.
Domain Model: westside-ops
Domain Model: westside-ops
westside-ops does not own a schema — it reads a curated subset of basketball-api's Postgres tables through the
westside_ops_reader role. This note documents which tables are exposed, how they group into the 9 Streamlit pages, and what is explicitly blocked.Diagram
Components
Exposed tables (14 total) — grouped by Streamlit page
| Streamlit Page | Primary Table | Joined Tables | Purpose |
|---|---|---|---|
| <strong>Players</strong> | <code>players</code> | <code>parents</code>, <code>teams</code> via <code>player_teams</code> | Full roster with contact info, jersey status, contract status, team assignment. Marcus's #1 daily view. |
| <strong>Parents</strong> | <code>parents</code> | <code>players</code> (aggregated count) | Parent contact list, waiver status, phone completeness. For collecting missing info. |
| <strong>Teams & Rosters</strong> | <code>teams</code> | <code>player_teams</code>, <code>players</code>, <code>coaches</code> | Per-team rosters (5 Kings teams, 2 Queens teams). Team assignment overview. |
| <strong>Contracts</strong> | <code>players</code> (contract fields) | <code>parents</code> | Contract lifecycle: none → offered → signed / declined. Marcus's chase list. |
| <strong>Jerseys & Orders</strong> | <code>orders</code> | <code>products</code>, <code>players</code>, <code>parents</code> | All purchases (jerseys, contract fees, tournaments). Status, payment, fulfillment. |
| <strong>Email Log</strong> | <code>email_log</code> | <code>parents</code>, <code>players</code> | Every email sent, by type, recipient, date. Transparency + audit. |
| <strong>Schedule</strong> | <code>events</code> + <code>practice_schedules</code> | <code>teams</code> | Tournaments, games, camps, tryouts, recurring practices. Per-team filtering. |
| <strong>Coaches</strong> | <code>coaches</code> | <code>teams</code> | Coach onboarding status, contractor agreement, contact info. |
| <strong>Sponsors</strong> | <code>sponsors</code> | — | Sponsor outreach status (44 sponsor_outreach emails already sent per email_log). <strong>Schema drift:</strong> this table exists in live DB but is not in basketball-api's <code>models.py</code>. See Key Decisions. |
Additional exposed tables used in joins or reference views:
tenants (for future multi-tenant filtering), registrations (for tryout payment history), products (for order pricing reference), interest_leads (for the public form submissions admin view), player_teams (junction).Forbidden tables (3 — never exposed)
| Table | Why blocked |
|---|---|
| <code>oauth_tokens</code> | <strong>Credential store.</strong> JSONB column contains Gmail access tokens and refresh tokens for <code>westsidebasketball@gmail.com</code>. Exposing this = full account compromise. Blocked at the Postgres role level, not just hidden in UI. |
| <code>password_reset_tokens</code> | Auth secrets. Short-lived but still privileged. No operator use case. |
| <code>outbox</code> | Internal event queue. Not data Marcus needs; exposing it would invite confusion and accidental edits to in-flight events. |
The westside_ops_reader role
Key Decisions
- Schema subset, not schema ownership. westside-ops reads basketball-api's tables directly via a read-only role. No ORM, no migrations, no shared models.py import. The Streamlit app issues raw SQL via psycopg2. This means the tool survives basketball-api schema changes without coordination — if basketball-api adds a column to
players, the StreamlitSELECT *picks it up on the next page load; if a column is renamed, the specific query mentioning it breaks visibly in one place. - 14 tables in, 3 tables out. The allowlist is explicit and narrow. Every exposed table has a clear Marcus-workflow justification in the Components table. The forbidden list contains only tables with production secrets or internal state. New basketball-api tables are not automatically exposed — adding a table to westside-ops requires a deliberate GRANT addition and a ticket.
- Grouping into 9 pages, not 60 views. Marcus's need is not "60 named saved views" — it's "a few pages with powerful in-grid filtering." Each Streamlit page shows the full dataset for a concern (all players, all parents, all contracts) and Marcus filters within the grid using
st.data_editor's built-in column filters. One "Contracts" page covers "unsigned contracts," "declined contracts," "Kings with offered contracts," and every other contract cohort Marcus might need — all via runtime filtering, zero developer involvement. - Schema drift finding:
sponsorstable +sponsor_outreachemail type exist in live DB but not inbasketball-api/src/basketball_api/models.py. Discovered during the scoping audit. westside-ops includessponsorsin its GRANT allowlist because the live workflow is active (44 sponsor_outreach emails sent per email_log). This is discovered scope for a separate basketball-api ticket — models.py should be brought back into sync with production, but that's not westside-ops's work to do. - tenant_id is always in the WHERE clause. Every query filters by
tenant_id = 1(Westside Kings & Queens) even though there's only one tenant today. This future-proofs the tool against the day a second tenant joins and prevents accidentally showing another org's data if multi-tenant goes live. - No read-your-writes consistency problem. Because westside-ops is read-only and the data source is the same Postgres cluster that basketball-api writes to, Marcus sees changes immediately after any basketball-api workflow completes. No cache, no sync, no staleness. The only lag is page refresh.
Related
arch-deployment-westside-ops— how this subset is served at runtimearch-dataflow-westside-ops— the query flow from Marcus's click to a rendered gridstory-westside-ops-spreadsheet-access— the user story this domain model servesnocodb-basketball-api-scoping(archived) — original schema audit, GRANT allowlist derivation, and NocoDB evaluation- basketball-api models:
~/basketball-api/src/basketball_api/models.py(source of truth for 17 of the 18 live tables)