Skip to main content

FS Garage Score Card (BE) Architecture

System Overview​

The Score Card backend is a small set of PocketBase collections layered on top of the existing projects collection: categories group metrics (with one level of subcategory nesting), metrics define what's tracked and its per-cadence targets, and entries hold the time-series values. This backend only stores data and enforces access/integrity rules - every roll-up, delta, and momentum calculation happens on the frontend (see FS Garage Score Card - Metrics and Calculations).

Architecture Diagram​

graph TB
subgraph BE["Backend: fs-garage-base"]
Projects["projects<br/>(existing collection, owner + members)"]
Categories["metric_categories<br/>display_type, color, restricted,<br/>self-referencing parent"]
Metrics["metrics<br/>aggregation, frequency,<br/>per-cadence targets, is_headline_kpi"]
Entries["metric_entries<br/>period_start <= period_end enforced"]
WriteRules["Scorecard write rules<br/>owner / ceo / c-suite / project-lead only"]
end
FE["Frontend: base-fs-garage-fe<br/>store/index.js + lib/scorecard/*.js"]

FE -->|"list/create/update"| Categories
FE -->|"list/create/update"| Metrics
FE -->|"list/create/update"| Entries
Categories -->|"scoped to"| Projects
Metrics -->|"belongs to"| Categories
Entries -->|"belongs to"| Metrics
WriteRules -->|"gate create/delete on"| Categories
WriteRules -->|"gate create/delete on"| Metrics
WriteRules -->|"gate create/delete on"| Entries

How to read this diagram​

Arrows mean flow / dependency / "talks to", not button clicks.

Core Components​

1785200001/2/3_created_metric_*.js​

The three foundational collection-create migrations: metric_categories (pbc_366275764), metrics (pbc_3017397517), metric_entries (pbc_724030684). See API Reference for the full field list of each.

1785200004_updated_metric_categories.js​

Adds the self-referencing parent relation to metric_categories as a follow-up migration rather than in the initial create - PocketBase can't create a collection with a relation field pointing at itself in the same step, so nesting support landed one migration later.

1785310445_updated_metric_entries_range_rule.js​

Despite the filename, this is a date-range integrity rule, not a numeric value range: it adds period_start <= period_end to metric_entries' createRule/updateRule, enforcing at the database level the same invariant the frontend's own entry form already checks - so the constraint holds even for a future client that skips the frontend's validation.

1785305698_updated_scorecard_write_rules.js​

Tightens createRule/deleteRule on all three Scorecard collections from "any authenticated user" (@request.auth.id != "") to project owner, or a global ceo/c-suite/project-lead role - a plain project member can view Scorecard data (via the existing listRule/viewRule, which does include project members) but cannot create or delete categories, metrics, or entries.

1786000200_updated_projects.js​

Adds a third value, business-unit, to the existing projects.type select field (previously client/product) - not a new field, just a new option consumed by the Score listing's project-type filter.

docs/scorecard-*.md​

Design/gap-analysis docs (scorecard-schema.md, scorecard-flow-feasibility.md, scorecard-dev-sync.md, scorecard-fsa-academy-seed-data.md) written to reconcile what the frontend expected against what existed in migrations at the time. The collections they flagged as missing have since been created by the migrations above.

Data Flow​

sequenceDiagram
participant FE as Frontend (Score Card)
participant PB as PocketBase

FE->>PB: GET metric_categories?project=:id
PB-->>FE: categories (incl. parent for nesting)
FE->>PB: GET metrics?category in (...)
PB-->>FE: metrics (aggregation, frequency, targets)
FE->>PB: GET metric_entries?metric in (...)
PB-->>FE: entries (period_start/period_end/value)
FE-->>FE: rollupWindow / computeDelta / presentMetric (lib/scorecard/*.js)

FE->>PB: POST metric_entries {metric, period_start, period_end, value}
PB-->>PB: check period_start <= period_end
PB-->>FE: created entry, or 400 on a bad range

Design Decisions​

Role-gated writes, member-visible reads​

Context: Every project member should be able to see a project's Score Card, but not everyone should be able to restructure it (add/remove categories, metrics, or entries).

Decision: listRule/viewRule stay open to the project's owner, members, and the ceo/c-suite/project-lead roles; createRule/deleteRule/updateRule narrow to just owner + those three roles. This was a deliberate tightening (updated_scorecard_write_rules.js) from an initial "any authenticated user can write" rule shipped with the first collection-create migrations.

Date-range integrity enforced at the database, not just the form​

Context: The frontend's entry form already rejects period_start > period_end, but that's not a guarantee if a script, migration, or future client writes directly to PocketBase.

Decision: Add period_start <= period_end to the collection's own createRule/updateRule, so the invariant holds regardless of which client is writing.

Nesting via a follow-up migration, not the initial create​

Context: metric_categories.parent is a self-relation, and PocketBase can't reference a collection's own not-yet-created ID within its create migration.

Decision: Create metric_categories first, then add parent in a second migration - a two-step pattern worth expecting again for any future self-referencing field.

Technology Stack​

LayerTechnology
RuntimePocketBase binary
DBSQLite (pb_data/)
Schemapb_migrations/*.js
Default branchdevelopment

Next Steps​