Government API
Government ledger, publishing, and administration route handlers
Every route in this page starts with a government session check. A government session is not enough for every mutation: readonly users, POCs, command leads, administrators, and system administrators have different gates. Examples use admin@navy.mil, sysadmin@navy.mil, NAVSEA, NIWC PAC, and Pelagic Dynamics.
Admin commands
GET /api/gov/admin/commands
Auth: admin-tier government session (GOV_COMMAND_LEAD, GOV_ADMIN, or GOV_SYSTEM_ADMIN). Success: 200.
curl http://localhost:3000/api/gov/admin/commands -H 'cookie: next-auth.session-token=SESSION'Example: { "data": [{ "id": "clx-navsea", "name": "Naval Sea Systems Command", "abbreviation": "NAVSEA", "echelon": "SYSCOM", "parentId": null, "location": "Washington, DC", "mission": "..." }] }.
Errors are 401, 403 where the route helper denies the role, and 500 for database failure.
POST /api/gov/admin/commands
Auth: GOV_SYSTEM_ADMIN; success: 201.
| Field | Type | Required? | Constraints |
|---|---|---|---|
name | string | Yes | Trimmed, 1–200. |
abbreviation | string | Yes | Trimmed, 1–50. |
echelon | string | Yes | Trimmed, 1–100. |
parentId | string/null | No | 1–255 or null. |
location | string/null | No | Max 200; nullish. |
mission | string/null | No | Max 4,000; nullish. |
curl -X POST http://localhost:3000/api/gov/admin/commands \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
-d '{"name":"Demo Maritime Integration Office","abbreviation":"DEMO MIO","echelon":"Office","parentId":"clx-navsea","location":"San Diego, CA","mission":"Coordinate pilot demonstrations."}'Success: { "data": { "id": "clx-command", "name": "...", "abbreviation": "DEMO MIO" } }. Errors: 400 invalid body, 401/403 gate, 409 duplicate name/abbreviation, 500.
GET|PATCH|DELETE /api/gov/admin/commands/[id]
Auth: GET uses government visibility; PATCH/DELETE require system-admin authority.
PATCH fields:
| Field | Type | Required? | Constraints |
|---|---|---|---|
name | string | No | Trimmed, 1–200. |
abbreviation | string | No | Trimmed, 1–50. |
echelon | string | No | Trimmed, 1–100. |
location | string/null | No | Max 200. |
mission | string/null | No | Max 4,000. |
curl -X PATCH http://localhost:3000/api/gov/admin/commands/clx-demo \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
-d '{"mission":"Updated pilot coordination mission"}'GET returns { "data": <command with relations> }; PATCH returns { "data": <updated command> }; DELETE returns { "data": { "id": "clx-demo" } }. GET errors are 401/403/404/500; mutation errors add 400 and 409. Delete can fail when relations or policy make the command ineligible.
PATCH /api/gov/admin/commands/[id]/parent
Auth: GOV_SYSTEM_ADMIN; success: 200.
| Field | Type | Required? | Constraints |
|---|---|---|---|
parentId | string/null | Yes | 1–255 or null. |
curl -X PATCH http://localhost:3000/api/gov/admin/commands/clx-nrl/parent \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
-d '{"parentId":"clx-onr"}'Success returns { "data": <updated command> }. Errors include 400 invalid body/self-parent/cycle, 401/403, 404 missing command/parent, and 500 transaction failure. The operation checks the graph before applying the parent.
Admin users
GET /api/gov/admin/users
Auth: admin-tier government session. Success: 200.
curl http://localhost:3000/api/gov/admin/users -H 'cookie: next-auth.session-token=ADMIN_SESSION'Response is { "data": [{ "id": "...", "email": "poc.ai@navy.mil", "name": "Dr. Marcus Vela", "role": "GOV_POC", "commandId": "...", "title": "AI/ML Portfolio Lead" }] }. Password hashes are not returned. Errors: 401/403/500.
POST /api/gov/admin/users
Auth: admin-tier policy; assignable roles are validated by the route.
| Field | Type | Required? | Constraints |
|---|---|---|---|
email | email string | Yes | Trimmed, max 255. |
name | string | Yes | Trimmed, 1–200. |
password | string | Yes | 14–128 and password regex. |
role | enum | Yes | Assignable government roles; cannot bypass tier policy. |
commandId | string/null | No | 1–255 or null. |
title | string/null | No | Max 200. |
curl -X POST http://localhost:3000/api/gov/admin/users \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"email":"demo.poc@navy.mil","name":"Demo POC","password":"Roundtable2026!Demo","role":"GOV_POC","commandId":"clx-niwcpac","title":"Pilot POC"}'Success 201 returns a sanitized user shape. Errors: 400 validation or role/command policy, 401/403, 409 duplicate email, 500.
PATCH /api/gov/admin/users
Auth: admin-tier policy.
| Field | Type | Required? | Constraints |
|---|---|---|---|
userId | string | Yes | 1–255. |
role | enum | No | Assignable role. |
commandId | string/null | No | 1–255 or null. |
title | string/null | No | Max 200. |
curl -X PATCH http://localhost:3000/api/gov/admin/users \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"userId":"clx-user","role":"GOV_COMMAND_LEAD","commandId":"clx-navsea","title":"NAVSEA lead"}'Success is { "data": <sanitized user> }. Errors: 400, 401/403, 404, policy protection for last system admin, and 500. The next session refresh sees changed authorization attributes from Prisma.
Publishing
POST /api/gov/announcements
Auth: government publisher (route helper). Success: 201.
| Field | Type | Required? | Constraints |
|---|---|---|---|
title | string | Yes | Trimmed, 1–200. |
body | string | Yes | Trimmed, 1–8,000. |
curl -X POST http://localhost:3000/api/gov/announcements \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"title":"ROUNDTABLE pilot update","body":"Industry office hours are available next week."}'Success { "data": { "id": "clx-announcement" } }. Errors: 400, 401/403, 500. The route creates a published row, audits it, and notifies industry users.
POST /api/gov/announcements/[id]/attachments
Auth: government publisher. Content type: multipart/form-data with one or more files parts. Success: 201.
| Constraint | Value |
|---|---|
| Allowed types | PDF, DOCX, PPTX, TXT, PNG, JPG, or JPEG; validated by filename extension and file contents. |
| Per-file size | 15 MB maximum. |
| Per-announcement count | 10 attachments maximum. |
curl -X POST http://localhost:3000/api/gov/announcements/clx-announcement/attachments \
-H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-F 'files=@briefing.pdf'Success returns { "data": [{ "id", "fileName", "contentType", "sizeBytes", "createdAt" }] }. Errors: 400 for missing files, disallowed type/content, an oversized file, or exceeding the attachment count; 401/403 for the session or publisher gate; 404 for a missing or deleted announcement; 413 for an oversized request body; 500 for a server error. Each upload is audited.
GET /api/gov/announcements/[id]/attachments/[attachmentId]
Auth: active signed-in session. Published announcement files are downloadable by any signed-in user; files on unpublished announcements are restricted to government sessions. Success: 200 with the stored file content type and attachment disposition.
curl http://localhost:3000/api/gov/announcements/clx-announcement/attachments/clx-attachment \
-H 'cookie: next-auth.session-token=SESSION'Errors: 401 when no active session is present, 404 for a missing/deleted or unauthorized attachment, and 500 for a server error. Successful downloads are audited. The route returns a generic not-found response for unauthorized records.
DELETE /api/gov/announcements/[id]/attachments/[attachmentId]
Auth: government publisher. Success: 200.
curl -X DELETE http://localhost:3000/api/gov/announcements/clx-announcement/attachments/clx-attachment \
-H 'cookie: next-auth.session-token=ADMIN_SESSION'Success returns { "data": { "id": "clx-attachment" } }; errors: 401/403 for the session or publisher gate, 404 for a missing/deleted or mismatched attachment, and 500 for a server error. Deletions are audited.
POST /api/gov/calls
Auth: government publisher. Success: 201.
| Field | Type | Required? | Constraints |
|---|---|---|---|
title | string | Yes | Trimmed, 1–200. |
description | string | Yes | Trimmed, 1–8,000. |
techTags | string | No | Max 400; normalized lowercase CSV. |
closesAt | date string | No | YYYY-MM-DD. |
status | enum | No | OPEN, DRAFT, or PENDING. |
releasesAt | date-time string | No | Required as a future time when status is PENDING. |
curl -X POST http://localhost:3000/api/gov/calls \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"title":"Maritime AI for Predictive Maintenance","description":"Share relevant maintenance analytics capabilities.","techTags":"ai/ml,predictive maintenance","closesAt":"2026-04-30"}'Success { "data": { "id": "clx-call" } }; errors 400, 401/403, 500. An OPEN call is public and notifies industry users; DRAFT and PENDING calls remain government-only. A pending call requires a future releasesAt.
PATCH /api/gov/calls/[id]
Auth: government publisher.
| Field | Type | Required? | Constraints |
|---|---|---|---|
status | enum | No | OPEN, CLOSED, DRAFT, or PENDING, subject to the transition rules below. |
releasesAt | date-time string | No | Future date-time required when the resulting status is PENDING; not valid for other resulting statuses. |
| Content fields | string/null | No | Draft and pending calls can update their editable title, description, tags, dates, solicitation, contact, office, and place-of-performance fields. |
curl -X PATCH http://localhost:3000/api/gov/calls/clx-call \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"status":"CLOSED"}'Allowed status transitions are DRAFT → PENDING, DRAFT → OPEN, PENDING → OPEN, PENDING → DRAFT, OPEN → CLOSED, and CLOSED → OPEN. A pending call whose releasesAt has passed is treated as open for lifecycle actions, so it can be closed but can no longer be edited or returned to draft. Opening a draft immediately or explicitly releasing a draft or pending call notifies industry; scheduling alone and reopening a closed call do not. A pending call that reaches its release time becomes visible at query time without a notification.
Content edits are accepted only while the effective status is DRAFT or PENDING. Status changes and content edits are audited. Success { "data": { "id": "clx-call", "status": "CLOSED" } }; errors 400 for invalid input, transition, or release date; 401/403 for the publisher gate; 404 for a missing call; and 500 for a server error.
Preset tags
GET /api/gov/admin/tags
Auth: GOV_SYSTEM_ADMIN only. Success: 200.
curl http://localhost:3000/api/gov/admin/tags \
-H 'cookie: next-auth.session-token=SYSTEM_SESSION'Success returns { "data": [{ "id", "label", "createdAt" }] }, sorted by normalized label. Errors: 401 unauthenticated, 403 authenticated but not a System Admin, and 500 for a server error.
POST /api/gov/admin/tags
Auth: GOV_SYSTEM_ADMIN only. Success: 201.
| Field | Type | Required? | Constraints |
|---|---|---|---|
label | string | Yes | Trimmed, lowercased, at most 50 characters, matching ^[a-z0-9][a-z0-9 \-\/&.+]{0,49}$. |
curl -X POST http://localhost:3000/api/gov/admin/tags \
-H 'content-type: application/json' \
-H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
-d '{"label":"Machine Learning"}'Success returns { "data": { "id", "label", "createdAt" } }; errors: 400 invalid input, 401 unauthenticated, 403 authenticated but not a System Admin, 409 when the normalized label already exists, and 500 for a server error. Creation is audited.
DELETE /api/gov/admin/tags/[id]
Auth: GOV_SYSTEM_ADMIN only. Success: 200.
curl -X DELETE http://localhost:3000/api/gov/admin/tags/clx-preset-tag \
-H 'cookie: next-auth.session-token=SYSTEM_SESSION'Success returns { "data": { "id": "clx-preset-tag" } }; errors: 401 unauthenticated, 403 authenticated but not a System Admin, 404 for a missing tag, and 500 for a server error. Deletion is audited.
Engagements
POST /api/gov/engagements
Auth: requireGovWrite; success 201.
| Field | Type | Required? | Constraints |
|---|---|---|---|
title | string | Yes | Trimmed, 1–200. |
type | enum | Yes | Nine EngagementType values. |
status | enum | Yes | PLANNED, COMPLETED, FOLLOW_UP, CLOSED. |
date | date string | Yes | YYYY-MM-DD. |
location | string | No | Max 200 or empty. |
summary | string | No | Max 4,000 or empty. |
organizationIds | string array | Conditionally | Up to 25 organization IDs, de-duplicated; every ID must exist. |
otherOrganization | string | Conditionally | Free-text participant name, max 200. |
commandId | string | Yes | Nonempty ID. |
At least one of organizationIds (non-empty array) or otherOrganization (non-blank) is required; otherwise the request is rejected with 400. The first selected organization is denormalized into Engagement.organizationId; all selected organizations are written as EngagementOrganization rows.
curl -X POST http://localhost:3000/api/gov/engagements \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"title":"Pelagic Dynamics autonomy demo","type":"DEMO","status":"PLANNED","date":"2026-03-20","location":"NIWC Pacific","summary":"Review UUV autonomy capability.","organizationIds":["clx-pelagic"],"commandId":"clx-niwcpac"}'Success { "data": { "id": "clx-engagement" } }. Errors: 400 field/foreign-key validation, 401/403 role/scope, 404 referenced record, 500. Current user becomes createdBy; engagement_created is audited.
PATCH /api/gov/engagements/[id]
Auth: requireGovWrite; request fields are the same fields above, with the same at-least-one-organization rule. The update replaces the engagement's EngagementOrganization rows with the new selection and keeps the denormalized organizationId in sync (preserving the existing primary organization when it is still selected).
curl -X PATCH http://localhost:3000/api/gov/engagements/clx-engagement \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"title":"Pelagic Dynamics autonomy demo","type":"DEMO","status":"COMPLETED","date":"2026-03-20","location":"NIWC Pacific","summary":"Completed technical demonstration.","organizationIds":["clx-pelagic"],"commandId":"clx-niwcpac"}'Success { "data": { "id": "clx-engagement" } }; errors 400, 401/403, 404, 500.
DELETE /api/gov/engagements/[id]
Auth: requireGovWrite. No body.
curl -X DELETE http://localhost:3000/api/gov/engagements/clx-engagement \
-H 'cookie: next-auth.session-token=ADMIN_SESSION'Success { "data": { "id": "clx-engagement" } }; errors 401/403, 404, 500. Related notes, organization links, and attachments are cascade-deleted with the engagement.
POST /api/gov/engagements/[id]/notes
Auth: requireGovWrite.
| Field | Type | Required? | Constraints |
|---|---|---|---|
body | string | Yes | Trimmed, 1–4,000. |
curl -X POST http://localhost:3000/api/gov/engagements/clx-engagement/notes \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"body":"Follow up with the autonomy portfolio lead."}'Success 201 { "data": { "id": "clx-note" } }; errors 400, 401/403, 404, 500.
POST /api/gov/engagements/[id]/attachments
Auth: requireGovWrite plus command scope on the engagement's command. Content type: multipart/form-data with one or more files parts.
| Constraint | Value |
|---|---|
| Allowed types | PDF, DOCX, PPTX, TXT, PNG, JPG/JPEG — validated by extension and magic bytes. |
| Per-file size | 15 MB. |
| Per-engagement total | 10 attachments, enforced inside a row-locked transaction (SELECT … FOR UPDATE) so concurrent uploads cannot exceed it. |
curl -X POST http://localhost:3000/api/gov/engagements/clx-engagement/attachments \
-H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-F 'files=@slides.pdf' -F 'files=@minutes.docx'Success 201 with { "data": [{ "id", "fileName", "contentType", "sizeBytes", "createdAt" }] }. Errors: 400 no files/disallowed type or contents/oversize/over the 10-attachment limit, 401 no session, 403 read-only role or out-of-scope command, 404 missing engagement, 500. Each stored attachment writes an engagement_attachment_uploaded audit event. File bytes are stored in the database (EngagementAttachment.data), not in S3.
GET /api/gov/engagements/[id]/attachments/[attachmentId]
Auth: requireGovRead. Streams the file back with its stored content type and an attachment disposition; the download is audit-logged. Errors: 401, 403 industry session, 404 wrong engagement/attachment pair, 500.
DELETE /api/gov/engagements/[id]/attachments/[attachmentId]
Auth: requireGovWrite plus command scope. Success { "data": { "id": "clx-attachment" } } and an audit event; errors 401/403, 404, 500.
Organization government routes
PATCH /api/gov/organizations/[id]/status
Auth: requireGovWrite.
| Field | Type | Required? | Constraints |
|---|---|---|---|
status | enum | Yes | NEW, UNDER_REVIEW, ACTIVE, WATCHLIST, INACTIVE. |
curl -X PATCH http://localhost:3000/api/gov/organizations/clx-pelagic/status \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"status":"ACTIVE"}'Success { "data": { "id": "clx-pelagic", "status": "ACTIVE" } }; errors 400, 401/403, 404, 500.
POST /api/gov/organizations/[id]/dialogue
Auth: requireGovWrite.
| Field | Type | Required? | Constraints |
|---|---|---|---|
body | string | Yes | Trimmed, 1–4,000. |
curl -X POST http://localhost:3000/api/gov/organizations/clx-pelagic/dialogue \
-H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
-d '{"body":"Coordinate a follow-up with the undersea portfolio."}'Success 201 { "data": { "id": "clx-dialogue-note" } }; errors 400, 401/403, 404, 500. This note is internal government context.
Government status and role summary
| Handler | Body/query | Passing audience | Typical denial |
|---|---|---|---|
| Admin command list | None | Government list policy | 401 or 403. |
| Admin command create/update/delete | JSON command fields | System admin | 403 for admin or command lead. |
| Parent reassignment | parentId string/null | System admin | 400 cycle, 403 role, 409 serialization conflict. |
| Admin users GET/POST/PATCH | User fields | Admin tier policy | 403 tier or protected-account rule. |
| Announcement create | title, body | Government publisher | 403 readonly/POC. |
| Call create/status | Call fields or status | Government publisher | 403 readonly/POC. |
| Engagement create/update/delete | Engagement fields | Government writer | 403 readonly/industry. |
| Engagement note | body | Government writer | 403 readonly/industry. |
| Organization status | status | Government writer | 403 readonly/industry. |
| Organization dialogue | body | Government writer | 403 readonly/industry. |
Error envelope examples
Invalid request:
{"error":"Invalid request"}Unauthorized:
{"error":"Unauthorized"}Forbidden:
{"error":"Forbidden"}Not found:
{"error":"Not found"}The exact punctuation varies between older portal handlers and shared government helpers, but callers should branch on HTTP status and treat the message as human-readable rather than a stable machine code.
Engagement date handling
The API accepts an ISO calendar date such as 2026-03-20, then stores a JavaScript DateTime. The UI and export code commonly display the date portion. Clients should send a date rather than a locale-formatted string such as 03/20/2026, because Zod's YYYY-MM-DD regular expression rejects locale formats.
Publishing side effects
Publishing an announcement or call has three observable outcomes:
- A row is persisted with the current government author or open status.
- An audit event records the operation and actor.
- Industry
Notificationrows may be created according to each recipient's settings.
If the database write succeeds but a notification fan-out fails, inspect server logs and the resulting record before retrying. Repeating a publish request can create another publication unless the client first checks current state.
Command safety
The parent endpoint accepts null to detach a command to the top level. It does not accept an empty string as a parent ID because the schema requires a nonempty string when non-null. The serializable transaction re-reads the current parent before writing so the audit detail reflects the value actually replaced.
Request construction notes
Government JSON routes reject malformed JSON before database work. Send content-type: application/json and serialize dates as YYYY-MM-DD. Do not send Prisma enum values with spaces or display labels: use exact values such as INDUSTRY_FORUM, FOLLOW_UP, UNDER_REVIEW, and GOV_SYSTEM_ADMIN.
IDs in examples are deliberately symbolic. Resolve them from a government list response or a seeded database; never infer a cuid from a display name. A route that accepts an ID still verifies that the referenced row exists and, where applicable, that the current role may operate on it.
Audit side effects by route family
| Family | Typical audit event or behavior |
|---|---|
| Command hierarchy | Reparent event includes previous and new parent IDs. |
| User administration | Role/assignment changes are recorded by admin helper. |
| Organization status | Status change includes organization and new status. |
| Dialogue and engagement notes | Author and parent record are attributable. |
| Engagement create/update | Creator and operation are recorded. |
| Announcement/call publication | Publisher and public title are recorded. |
The audit event name is more stable than the human-facing success body. Consumers should use HTTP status and their own operation ID rather than scraping an error sentence.
Permission test matrix
| Test caller | Request | Expected |
|---|---|---|
| Industry contact | POST /api/gov/engagements | 403. |
GOV_READONLY | POST /api/gov/announcements | 403. |
GOV_POC | POST /api/gov/calls | 403 under publisher policy. |
lead.navsea@navy.mil | Edit NSWC DD | 403 exact-scope denial. |
admin@navy.mil | Reparent command | 403 system-admin-only. |
sysadmin@navy.mil | Reparent valid command | 200 if body and cycle checks pass. |
lead.navsea@navy.mil | Export CSV | 403 enterprise-export gate. |
admin@navy.mil | Export CSV | 200. |
Use disposable records for mutation tests. A 403 is evidence of the gate only when the request reached the intended route and the session identity is the account described.
