# Platform Service -- TRD

## Status

In Progress -- 2026-04-10

## Overview

### The problem

Coda is transitioning from internal beta to a white-label external product. The current authorization model -- stateless JWT roles from the Grass identity platform plus email-based admin allowlists -- is insufficient for enterprise customers. There is:

- **No DB-backed permissions.** Grass JWT roles are used informally. Search admin access is gated by an `SEARCH_ADMIN_EMAILS` env var.
- **No tenant isolation.** All users share a single implicit context. There is no way to scope data, permissions, or configuration to a customer organization.
- **No audit logging.** Permission checks are not recorded. There is no trail for compliance review.
- **No feature gating.** Every user has access to every feature. There is no mechanism for plan-based access tiers.

Enterprise-level access control, multi-tenancy, audit compliance, and feature gating are prerequisites for productionization.

### The solution

A dedicated ConnectRPC microservice (`apps/platform`) that owns all authorization, multi-tenancy, compliance, and entitlement management. The app server and search service call it via `@coda/admin-api` for permission checks on every request.

This provides:

- **RBAC + ABAC** -- hierarchical roles with typed policy conditions (ownership, department, time window, resource state, IP range)
- **Multi-tenancy** -- explicit `TenantUser` membership, tenant-scoped everything, DB-level cross-tenant isolation
- **Audit logging** -- append-only, async, non-blocking, with crypto-shredding for GDPR compliance
- **Feature gating** -- plan-based feature keys checked before permission resolution
- **Deny-wins** -- explicit deny overrides that cannot be bypassed, even by super admins
- **Shadow mode** -- gradual rollout with log-only permission checks before enforcement

---

## Architecture

### System context

```
                 ConnectRPC (internal VPC, port 8082)

 ows-coda server ──────────────┐
 (Fargate)                     |
                               v
 ows-coda search ─────────>  [Platform Service (Fargate)]
                               |       |            |
                               v       v            v
                             Aurora   Redis         S3
                             MySQL   (cache +     (audit
                                     audit buf)   archive)
```

All consumers use `@coda/admin-api` to call the platform service. The hot-path RPC (`Check`) targets <5ms p99 cached.

### Service internals

```
apps/platform/src/
  index.ts              Entrypoint: init providers, create domains, start server
  server.ts             Express + ConnectRPC middleware + health endpoints
  config/
    load-config.ts      Zod-validated env vars -> typed PlatformConfig

  domains/access/       Access domain handlers + service logic
    handlers/
      access.ts         AccessService RPC handlers (hot path)
    service/
      permission-resolver.ts  16-step resolution pipeline
      role-hierarchy.ts       Recursive role inheritance resolution
      sod-validator.ts        Separation of duty constraint checker
      cache.ts                Redis cache read/write/invalidation
      condition-evaluators/   ABAC condition type implementations

  domains/platform/     Platform domain handlers + service logic
    handlers/
      tenant.ts         TenantService handlers
      identity.ts       IdentityService handlers

  domains/compliance/   Compliance domain handlers + service logic
    service/
      audit-writer.ts   Redis buffer + crypto-shredding
      audit-drainer.ts  Buffer -> DB batch insert

  events/               Inter-domain event bus
    event-bus.ts        Typed EventEmitter
    events.ts           Event type definitions
```

### Request flow

```
ConnectRPC request (e.g. Check)
  |
  v
[AccessService handler] -- validates input
  |
  v
[PermissionResolver.resolve()]
  |
  +-- Steps 3-4: Tenant + user status (cache -> DB)
  +-- Step 7: Plan feature gating
  +-- Step 8: Deny overrides (DENY-WINS)
  +-- Step 9: Super admin
  +-- Steps 10-11: Direct grants + role permissions
  +-- Step 13: ABAC conditions
  +-- Step 16: GRANTED
  |
  v
[AuditWriter.write()] -- fire-and-forget
  |
  v
Response: { outcome, reason, missingPermission }
```

---

## API

Source of truth: `packages/admin-api/proto/coda/admin/v1/access.proto`

### Core RPCs (hot path)

| RPC                                                         | Purpose                                                   |
| ----------------------------------------------------------- | --------------------------------------------------------- |
| `Check(user_id, tenant_id, permission, context)`            | Single permission check. Called on every API request.     |
| `CheckBatch(user_id, tenant_id, permissions)`               | Batch check. Used by UI to preload permission state.      |
| `GetEffective(user_id, tenant_id)`                          | Full effective permission set. Called on login.           |
| `ResolveTenant(identity_id, email, applications, profiles)` | Map Grass JWT to tenant context. Called once per request. |

### Transport

ConnectRPC on port 8082. Connect JSON (HTTP/1.1) and native gRPC (HTTP/2). Compression: gzip and brotli.

### Health checks

- `GET /health` -- always 200
- `GET /health/ready` -- 200 when cache and DB are healthy, 503 otherwise

---

## Packages

### `@coda/admin-api` -- proto contract and client

Proto definitions for the access domain. Generated TypeScript types via buf. Client factory with timeout and error handling. This is the only package most consumers need.

### `@coda/admin-api` -- tenant and identity management

Proto definitions for tenant lifecycle, user management, invitations, groups, and branding.

### `@coda/compliance-api` -- audit and consent

Proto definitions for audit log queries, consent tracking, and privacy requests.

### `@coda/common-api` -- shared types

Pagination messages (`PageRequest`, `PageResponse`), shared enums, field mask re-exports.

---

## Configuration

All configuration is via environment variables, validated by Zod at startup.

| Variable                  | Default  | Purpose                                                       |
| ------------------------- | -------- | ------------------------------------------------------------- |
| `PORT`                    | 8082     | HTTP listen port                                              |
| `REDIS_URL`               | _(none)_ | Redis for cache + audit buffer. Omit for NullPermissionCache. |
| `SHADOW_MODE`             | true     | Log-only permission checks                                    |
| `AUDIT_MASTER_KEY`        | _(none)_ | 256-bit hex key for audit PII encryption                      |
| `PIPELINE_STEP_*_ENABLED` | false    | Feature flags for Phase 2 pipeline steps                      |

---

## Graceful degradation

| Failure               | Behavior                                                                                                  |
| --------------------- | --------------------------------------------------------------------------------------------------------- |
| Redis unavailable     | Permission checks fall through to DB. Rate limits fail closed (429). Audit writes fall back to pino JSON. |
| Aurora DB unavailable | All permission checks return DENIED (fail-closed).                                                        |
| Audit buffer full     | Drainer processes up to 1,000/tick. If buffer exceeds 10K, sync fallback kicks in.                        |
| Shadow mode           | Permission results are logged but not enforced.                                                           |

---

## Testing

### Unit tests

Located in domain-specific `__tests__/` directories:

| Area                 | Test files                                                                                                                                              |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Permission resolver  | `permission-resolver.test.ts`, 5 invariant suites (`inv-fail-closed`, `inv-deny-wins`, `inv-tenant-scoped`, `inv-role-cycle`, `inv-superadmin-ceiling`) |
| Cache                | `cache.test.ts`                                                                                                                                         |
| Role hierarchy       | `role-hierarchy.test.ts`                                                                                                                                |
| SoD validator        | `sod-validator.test.ts`                                                                                                                                 |
| Condition evaluators | `condition-evaluators.test.ts`                                                                                                                          |
| Audit writer         | `audit-writer.test.ts`                                                                                                                                  |
| Audit drainer        | `audit-drainer.test.ts`                                                                                                                                 |
| Server               | `server.test.ts`                                                                                                                                        |
| Config               | `config.test.ts`                                                                                                                                        |
| Handlers             | `access.test.ts`, `tenant.test.ts`, `identity.test.ts`                                                                                                  |
| Event bus            | `event-bus.test.ts`                                                                                                                                     |

Total: 260+ tests. 100% coverage on `permission-resolver.ts`.

### Review history

The pipeline underwent 5 greenfield review rounds:

| Round | Issues Found                  |
| ----- | ----------------------------- |
| V1    | 12 (1 critical, 3 high)       |
| V2    | 5 (2 medium, 3 low)           |
| V3    | 5 design + 24 test gaps       |
| V4    | 6 design + 7 test gaps        |
| V5    | 0 (clean -- production-ready) |

---

## Decision log

| Decision                           | Rationale                                                                                                              |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Standalone service over in-process | Authorization must be consistent across all services. A shared service ensures one source of truth.                    |
| ConnectRPC over REST               | Type-safe RPC with protobuf. Same port serves Connect JSON and gRPC. Consistent with search service.                   |
| 4 domains in 1 process             | Simpler deployment today. Proto package split ensures future decomposition is routing, not rewriting.                  |
| Redis cache with DB fallback       | <5ms p99 target requires caching. DB fallback preserves correctness when cache is unavailable.                         |
| Synchronous cache invalidation     | Stale grants are security vulnerabilities. Accept synchronous DEL latency on writes.                                   |
| Crypto-shredding for audit PII     | Reconciles immutable audit trail (SOC 2) with GDPR erasure. Structural data preserved, PII destroyed via key deletion. |
| Shadow mode rollout                | Validate against real traffic before enforcement. Prevents misconfiguration lockouts.                                  |
| Deny-wins before super admin       | Deny overrides are security controls. Super admin privilege cannot bypass them.                                        |
| Feature-flagged pipeline steps     | Phase 1 stubs for steps 5, 6, 12, 14, 15. Progressive enablement avoids big-bang risk.                                 |
| Null-object cache pattern          | NullPermissionCache avoids conditional checks. Clean testing without Redis.                                            |

---

## Alternatives considered

| Alternative                                    | Why rejected                                                                                           |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **OPA/Rego policy engine**                     | Requires learning a DSL. Typed condition evaluators are more maintainable and testable in TypeScript.  |
| **Casbin**                                     | Good for simple RBAC but awkward for hierarchical roles + ABAC conditions + tenant isolation.          |
| **Auth0 Fine-Grained Authorization**           | Vendor lock-in. Cannot customize the permission pipeline. Latency concerns for every-request checks.   |
| **Permissions in the app server**              | No single source of truth. Each service would need its own permission logic. Inconsistent enforcement. |
| **Eventual consistency on cache invalidation** | Stale grants are security vulnerabilities. Synchronous invalidation is the only safe option.           |

---

## Related documents

- [Platform Architecture](../../architecture/platform.md) -- full architecture deep dive
- [Platform Security](../../compliance/security.md) -- security design
- [Enterprise Permissions PRD](../prds/enterprise-permissions.md) -- product requirements
