# Valuenomics Control Plane

Version: 1.0  
Telemetry schema: `1`  
Owner: Valuenomics platform team

## Purpose

This is the master management contract for every Valuenomics application. It creates one operational view of:

- who can sign in and which tools they can use;
- access requests, approvals, rejections and blocked accounts;
- app launches, active users, volume, frequency and retention;
- coarse access location by country or business region;
- notifications delivered through the portal;
- data quality and reporting coverage across the toolset.

Applications do not create their own management schemas. Every app uses the shared identity, entitlement and telemetry contracts below.

## Control-plane architecture

```text
Valuenomics apps
  -> Firebase Authentication (one UID)
  -> callable trackEvent function (validated event envelope)
  -> Firestore events (recent operational data)
  -> daily rollups (fast dashboard queries)
  -> BigQuery export (long-term analysis at scale)

Management console
  -> callable admin functions
  -> Firebase Admin SDK
  -> Authentication users, claims, Firestore access records and notifications
```

The browser never receives service-account credentials and never calls the Admin SDK. Approve, reject, block, restore and publish operations run in callable Cloud Functions after checking the authenticated user's `admin: true` custom claim.

## Canonical application registry

| `appId` | Display name | Current destination |
| --- | --- | --- |
| `portal` | Valuenomics Portal | Portal origin |
| `use-case-mapper` | Use Case Mapper | `https://usecase.valuenomics.co.uk/` |
| `resilience` | Resilience | `https://resilience.valuenomics.co.uk/` |
| `renew` | Renew | `https://igelrenewals.valuenomics.co.uk/` |
| `persona` | Persona Modelling | `https://workforce.valuenomics.co.uk/` |
| `advisory` | Advisory | Pending |
| `assurance` | Assurance | Pending |
| `executive` | Executive | Pending |

Never rename a released `appId`. Display names and URLs may change without breaking analytics history.

## Master event envelope

Every app sends this envelope through `trackEvent`. The callable function supplies trusted identity and coarse location fields; clients must not send or override them.

```json
{
  "schemaVersion": 1,
  "eventName": "assessment_completed",
  "occurredAt": "2026-07-19T15:42:10.000Z",
  "appId": "resilience",
  "environment": "production",
  "sessionId": "57f940fd-07ee-49ed-bf68-d9b1b334183e",
  "context": {
    "customerId": "internal-non-sensitive-reference",
    "workspaceId": "workspace-reference",
    "source": "portal"
  },
  "properties": {
    "assessmentType": "operational-resilience",
    "durationSeconds": 326
  }
}
```

The ingestion function adds:

```json
{
  "receivedAt": "server timestamp",
  "actor": {
    "uid": "Firebase UID",
    "email": "authenticated email",
    "role": "role custom claim"
  },
  "geo": {
    "country": "GB",
    "region": "coarse infrastructure region"
  }
}
```

### Required fields

| Field | Rule |
| --- | --- |
| `schemaVersion` | Integer `1` |
| `eventName` | Lowercase snake case, maximum 64 characters |
| `occurredAt` | ISO timestamp, no more than 24 hours from receipt |
| `appId` | Value from the canonical registry |
| `environment` | `production`, `staging` or `development` |
| `sessionId` | Random identifier, rotated after an extended inactive period |
| `context` | Shared cross-app references; never free-form personal data |
| `properties` | Event-specific values defined in the event catalogue |

## Core event catalogue

All apps implement the platform events. Apps implement domain events where relevant.

| Event | When emitted | Required properties |
| --- | --- | --- |
| `session_started` | Authenticated app session begins | `source` |
| `app_launch` | User selects an app in the portal | `targetAppId` |
| `app_viewed` | App shell is ready | `version` |
| `feature_used` | Material workflow action | `featureId` |
| `workspace_created` | New customer/value workspace created | `workspaceType` |
| `workspace_opened` | Existing workspace opened | `workspaceId` |
| `assessment_started` | Assessment begins | `assessmentType` |
| `assessment_completed` | Assessment is completed | `assessmentType`, `durationSeconds` |
| `value_model_created` | Financial/value model created | `modelType` |
| `report_exported` | Report or output exported | `format`, `reportType` |
| `notification_opened` | Portal notification selected | `notificationId` |
| `access_requested` | Access form submitted | `role`, `region` |
| `sign_in_failed` | Authentication fails | `reasonCode` |

Do not use an event name to encode values. Use `feature_used` with `properties.featureId`, not separate events such as `clicked_blue_button`.

## Identity and access model

### Firebase Authentication

- One Firebase project owns the canonical UID.
- Google is the primary identity provider.
- `users/{uid}` stores status, role, region and tool entitlements.
- Custom claims contain only compact authorization facts such as `admin`, `approved` and role identifiers.
- Firestore remains the source of truth for detailed entitlements.

### User status

| Status | Authentication | Tool access |
| --- | --- | --- |
| `invited` | Permitted to complete sign-in | None until approved |
| `active` | Enabled | Entitled tools only |
| `blocked` | Firebase user disabled and refresh tokens revoked | None |
| `rejected` | No approval | None |

Blocking is a privileged server action using the Firebase Admin SDK. The console records the administrator UID, target UID, reason and timestamp in `auditLog`.

### Access decision workflow

1. User submits an access request.
2. The request is written to `accessRequests` with status `pending`.
3. Administrator reviews role, region, reason and requested tools.
4. Approve or reject runs through `decideAccess`.
5. Approval updates the user profile and authorization claims.
6. Every decision creates an immutable audit event.

## Location and privacy

Location reporting is coarse and server-derived. Use country or IGEL business region only.

- Do not request browser GPS permission.
- Do not store raw IP addresses in analytics events.
- Do not use location to infer sensitive behaviour.
- Restrict management-console access to authorised administrators.
- Define retention for raw events; retain aggregated metrics longer than user-level detail.
- Provide a documented process for account deletion and analytics pseudonymisation.

## Firestore collections

| Path | Purpose | Browser write access |
| --- | --- | --- |
| `users/{uid}` | Status, role, region and entitlements | None |
| `accessRequests/{id}` | Approval queue | Protected function only |
| `events/{id}` | Validated raw operational events | Callable ingestion only |
| `metricsDaily/{date}/shards/{shard}` | Distributed daily total counters | Trigger only |
| `metricsDaily/{date}/apps/{appId}/shards/{shard}` | Distributed app daily counters | Trigger only |
| `metricsDaily/{date}/regions/{region}/shards/{shard}` | Distributed region counters | Trigger only |
| `metricsDaily/{date}/users/{uid}` | Daily user activity | Trigger only |
| `notifications/{id}` | Global portal alerts | Admin function only |
| `users/{uid}/notifications/{id}` | Private or targeted alerts | Admin function only |
| `auditLog/{id}` | Privileged action record | Admin function only |
| `dashboardSnapshots/current` | Precomputed management summary | Scheduled function only |

## Aggregation strategy

Use two reporting tiers:

1. **Operational dashboard:** Firestore daily rollups provide fast counts for the management console without scanning raw events.
2. **Long-term analysis:** Export raw event data to BigQuery for cohort analysis, retention, cross-app journeys and high-volume history.

Counters are distributed across 20 shards rather than updating one hot document for every event. A slower snapshot job sums those shards for the console. Unique active users are counted from `metricsDaily/{date}/users` documents, not from a growing array inside one document. Together these choices avoid document-size and write-contention problems.

Suggested scheduled jobs:

- every 15 minutes: refresh `dashboardSnapshots/current`;
- daily: validate event coverage and quarantine schema exceptions;
- monthly: expire raw events beyond the agreed retention period;
- daily: reconcile Authentication users with `users/{uid}` profiles.

## Notification contract

```json
{
  "active": true,
  "title": "Use Case Mapper update",
  "message": "A new prioritisation workflow is available.",
  "url": "https://usecase.valuenomics.co.uk/",
  "audience": "all",
  "createdAt": "server timestamp",
  "createdBy": "administrator UID"
}
```

For translated content, `title` and `message` may be maps keyed by supported locale. Global alerts go to `notifications`. Region or user-specific alerts are fanned out to the private user subcollection by the publishing function.

## App integration

Include `telemetry.js`, then configure the stable app identifier:

```html
<script src="https://www.valuenomics.co.uk/firebase-config.js"></script>
<script src="https://www.valuenomics.co.uk/telemetry.js"></script>
<script>
  ValuenomicsTelemetry.configure({appId: 'resilience'});
  ValuenomicsTelemetry.track('app_viewed', {version: '1.4.0'});
</script>
```

Track a domain event:

```js
ValuenomicsTelemetry.track('assessment_completed', {
  assessmentType: 'operational-resilience',
  durationSeconds: 326
}, {
  workspaceId: 'workspace-reference',
  customerId: 'non-sensitive-reference'
});
```

### New-app onboarding checklist

- Register a stable `appId`.
- Use the shared Firebase project and verify the same UID.
- Enforce entitlement checks server-side or in Security Rules.
- Install the shared telemetry client.
- Emit all relevant platform events.
- Add domain events to this catalogue before release.
- Pass schema validation in staging.
- Confirm no raw IP address, GPS location or unrestricted personal data is emitted.
- Confirm the management console shows launches, users, region, frequency and last activity.
- Assign an app owner and data-quality owner.

## Production controls

- Enable App Check for callable functions.
- Set the `admin: true` claim only through a controlled bootstrap process.
- Require step-up authentication for high-risk admin actions if available.
- Revoke refresh tokens when an account is blocked.
- Log every privileged action.
- Never expose Admin SDK credentials to any browser.
- Use budgets, quotas and retention policies before high-volume rollout.
- Test Firestore rules with the emulator before deployment.

## Official implementation references

- Firebase Admin user management: <https://firebase.google.com/docs/auth/admin/manage-users>
- Callable Cloud Functions: <https://firebase.google.com/docs/functions/callable>
- Firestore real-time listeners: <https://firebase.google.com/docs/firestore/query-data/listen>
- Firestore usage and limits: <https://firebase.google.com/docs/firestore/quotas>
- Firestore distributed counters: <https://firebase.google.com/docs/firestore/solutions/counters>
- Firestore aggregation queries: <https://firebase.google.com/docs/firestore/query-data/aggregation-queries>
