Meridian Field Services: instructional example, not project evidence
The style is a decision, the conventions are stated once, and the spec is the source of truth; everything below references this section instead of re-deciding.
What: resource-oriented HTTP, a graph query layer, binary RPC. Why considered: consumers are the board (browser), the technician mobile app, and one inbound vendor webhook; no cross-resource aggregation need exists (the board's read model is server-composed) and vendor tooling expects plain HTTP. Who & how: E. Sandoval with A. Reyes, walked against the four screens and the integration register, July 16. Findings: GraphQL rejected (no aggregation consumer; cache and authorization complexity unjustified); gRPC rejected (browser and vendor compatibility cost) (API-style decision[1]).
Spec-first: the OpenAPI file lives at api/openapi.yaml, pinned per release; reference docs and client types generate from it; this artifact carries the invariants, the registry, and the matrix.
| Convention (stated once) | Rule |
|---|---|
| Naming | snake_case fields · plural collections (/jobs, /assignments) · nesting ceiling 2 |
| Pagination | page_size (max 200) + page_token → next_page_token |
| Filtering / ordering | filter= · order_by= per the list convention |
| Versioning | v1; breaking = rename or removal, new required field, changed status or error semantics; 180-day deprecation window with Deprecation + Sunset headers; no URI versioning (versioning decision[1]) |
Resources: jobs, technicians, assignments, positions, suggestions (five roots, under the ceiling of eight); each maps to its data-model entity (entities[2]).
Every row carries its example, its errors, its auth citation, and its SLO class. Limits are numbers because they protect the SLO, and say so.
| Class | p95 budget | Source |
|---|---|---|
| read | 300 ms | quality scenarios[3] (cited, not invented) |
| write | 500 ms | |
| computed | 800 ms (inside the sub-second suggestion budget) |
Rate limits: staff sessions 600 req/min (burst 900); vendor webhook ingress 900/min (burst 1500, from the feed contract); mobile 120/min.
| Endpoint | Purpose | Auth (cites policy) | Errors | Class | Example |
|---|---|---|---|---|---|
| GET /jobs | List jobs; filters status, hub; pagination convention §1 | dispatcher, ops manager[4] | validation_failed · rate_limited | read | EX-1 |
| POST /assignments | Create assignment; Idempotency-Key required: 24-h retention; reuse with different parameters returns conflict_key_reuse (US-1[5]) | dispatcher · hub scope[4] | authority_denied · stale_version | write | EX-3 |
| POST /overrides | Record override; reason_code required (FR-013[6]) | dispatcher[4] | validation_failed · authority_denied | write | EX-4 |
| GET /jobs/{id}/suggestions | Ranked candidates; degradation contract §4 | dispatcher[4] | suggestions_unavailable | computed | EX-5 |
| GET /positions | Vehicle positions; band-masked off dispatch surfaces per the handling matrix (labels[7]) | dispatcher, fleet manager[4] | rate_limited | read | EX-6 |
| POST /webhooks/positions | Vendor feed ingress: contract in §5 | machine · vendor signature[4] | signature_invalid · rate_limited | write | EX-7 |
Every example is a tested request/response pair in the example set (example set[8]); payload fields cite data-model ids, never re-describe them.
The policy owns the roles; this artifact owns which endpoint group needs which. Nothing is redefined here.
| Endpoint group | Policy role (cited) | Note |
|---|---|---|
| jobs read | dispatcher · ops manager[4] | ops manager read-only; fleet manager denied |
| assignments create | dispatcher[4] | within hub scope, server-enforced |
| overrides create | dispatcher[4] | reason required |
| positions read | dispatcher · fleet manager[4] | masking per classification |
| feed configuration | fleet manager[4] | none |
| role administration | platform admin[4] | no wildcard; the policy's matrix law |
Enforcement: server-side denial, not hidden buttons (SR-004[9]). Machine access: the positions webhook authenticates by vendor signature with quarterly rotation (machine identity[4], SR-021[9]); the notification worker and adapter use platform service accounts with short-lived tokens.
One envelope: RFC 9457 problem details. One closed registry: a new type is a review with the API owner, never an author's invention. Type URIs under api.meridianfield.example/errors/.
| Type | Status | When raised | Driver |
|---|---|---|---|
| validation_failed | 422 | Malformed or out-of-contract input | SR-006[9] |
| authority_denied | 403 | Role or scope check failed server-side | SR-004[9] · TM-03[10] |
| not_found | 404 | Resource id does not resolve within the caller's scope | none |
| stale_version | 409 | Optimistic-lock version mismatch | concurrency[2] |
| conflict_key_reuse | 409 | Idempotency-Key reused with different parameters | §2 rule |
| rate_limited | 429 | The tier's numeric limit; Retry-After carried | §2 limits |
| signature_invalid | 401 | Webhook signature or replay-window failure | SR-021[9] |
| suggestions_unavailable | 503 | Degradation contract: the ranked-suggestion path degraded; the board proceeds with the full list | degradation posture[3] |
The degradation row exists because the architecture commits the fallback; clients build against it instead of discovering it in production.
Verify before you parse; reconcile on state, never on arrival order.
| Rule | Vendor positions webhook (inbound) |
|---|---|
| Signature | HMAC-SHA256 over the raw body + timestamp header; replay window 300 s; verified before parsing or any side effect (SR-021[9], TM-01[10]) |
| Delivery | At-least-once; vendor exponential backoff over 6 h; receiver deduplicates on the vendor event id |
| Ordering | Arrival order untrusted: the adapter reconciles on observed_at, never arrival sequence (INT-1[3]) |
| Receiver responses | 2xx accepted · 401 signature_invalid = poison (no retry) · 429 / 5xx = retry |
Outbound: none at pilot; assignment notifications flow through the notification gateway (INT-2[3]), not webhooks; stated so the absence is a claim.
| Question | Owner | Answer by | Blocks |
|---|---|---|---|
| Does the vendor's certificate-rotation drill need a partner sandbox before the quarterly rotation (machine identity[4])? | E. Sandoval | Sep 5, 2026 | The rotation runbook only, not the contract |