Skip to content

Operations Runbook

Active operational procedures for partner API keys and person affiliation updates.

This runbook documents active operations for API keys, partner writes, and person affiliation management.

Partner write access for https://api.legaciti.org is controlled by scoped API keys in api_keys.

Supported scopes:

  • ingest for POST /api/ingest
  • membership.update for POST /api/partners/people/:orcid/membership

All endpoints require a valid Better Auth session and superadmin privileges.

  • GET /api/site-tools/api-keys
  • POST /api/site-tools/api-keys
  • PATCH /api/site-tools/api-keys/:keyHash
  • DELETE /api/site-tools/api-keys/:keyHash
  • POST /api/site-tools/api-keys/:keyHash/rotate

Create a key:

Terminal window
curl -X POST https://my.legaciti.org/api/site-tools/api-keys \
-H "Cookie: better-auth.session_token=$SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"client_name": "partner membership bot",
"scopes": ["membership.update"],
"rate_limit_per_minute": 60
}'

Rotate and revoke old key:

Terminal window
curl -X POST https://my.legaciti.org/api/site-tools/api-keys/<key_hash>/rotate \
-H "Cookie: better-auth.session_token=$SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"revoke_old": true}'

Partners call the public API with X-API-Key.

Terminal window
curl -X POST "https://api.legaciti.org/api/partners/people/<orcid>/membership" \
-H "X-API-Key: $PARTNER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity_id": "cesam",
"status": "cesam"
}'

Request body:

  • entity_id must be cesam
  • status is cesam or external
  • starts_at is optional and uses unix milliseconds

Expected responses:

  • 200 affiliation update stored successfully
  • 400 invalid request body
  • 401 missing, invalid, or inactive key
  • 403 valid key without required scope
  • 404 person or entity not found

Internal affiliation routes:

  • GET /api/people
  • GET /api/people/:orcid
  • PATCH /api/people/:orcid/affiliation

Affiliation update payload:

{
"entity_id": "cesam",
"status": "cesam",
"starts_at": 1711270800000
}

Behavior:

  • prior active period is closed by setting ends_at
  • new active period is inserted with ends_at = null

GET /api/people supports:

  • q
  • page
  • perPage
  • sort as name or last_fetched_at
  • dir as asc or desc
  • entity_id
  • affiliation_status as cesam or external
  • active_only as true or false (default false)
  • category_id

Dashboard activity endpoints:

  • GET /api/activity
  • POST /api/activity/cleanup

Access model:

  • Read access: any workspace member role
  • Cleanup access: only workspace_admin and superadmin

Default timeline behavior:

  • If from and to are omitted, /api/activity returns the last 30 days.

Key event families:

  • Publication edits and lifecycle: publication.edited, publication.deleted, publication.restored, publication.visibility_changed
  • Workspace membership changes: workspace.user.invited, workspace.user.invite_cancelled, workspace.user.removed, workspace.user.role_updated
  • Ingestion requests and processing: ingestion.requested, ingestion.job.started, ingestion.job.completed, ingestion.job.partial_failure, ingestion.work.failed, ingestion.job.failed
  • Activity cleanup action: workspace.settings.activity_cleared

Severity and status conventions:

  • severity: info, warn, error
  • status: success, partial, failure

Most recent 200 failures in a workspace:

SELECT id, occurred_at, event_type, actor_id, message, metadata_json
FROM activity_events
WHERE entity_id = ? AND status = 'failure'
ORDER BY occurred_at DESC
LIMIT 200;

Partial ingestion failures in last 24 hours:

SELECT occurred_at, resource_id AS job_id, message, metadata_json
FROM activity_events
WHERE entity_id = ?
AND event_type = 'ingestion.job.partial_failure'
AND occurred_at >= ?
ORDER BY occurred_at DESC;

Cleanup actions audit:

SELECT occurred_at, actor_id, message, metadata_json
FROM activity_events
WHERE entity_id = ?
AND event_type = 'workspace.settings.activity_cleared'
ORDER BY occurred_at DESC
LIMIT 100;
  • Require an explicit maintenance ticket before running cleanup.
  • Prefer mode=before_date over mode=all to preserve recent diagnostics.
  • Capture reason and approver in the maintenance ticket before execution.
  • Validate cleanup impact by comparing counts before/after and recording deleted_count.

Production Verification Checklist (Worker Activity)

Section titled “Production Verification Checklist (Worker Activity)”

After deploying the dashboard (my.legaciti.org), worker-ingestion-orchestrator, and worker-ingestion-process:

  1. Trigger ingestion via POST /api/ingest and verify ingestion.requested appears in /api/activity.
  2. Verify worker-generated events (ingestion.job.started plus completion or failure) are present for the same workspace.
  3. Confirm workspace scoping by checking events are not visible from another workspace context.
  4. Verify workspace_member cannot call POST /api/activity/cleanup (expect 403).
  5. Verify workspace_admin can call cleanup and that workspace.settings.activity_cleared is logged.
  6. Confirm Activity page renders newly generated events and filters correctly in production UI.

Person not visible in default filtered views:

  • Verify active affiliation for the current workspace entity.
  • Patch affiliation to status: "cesam" when needed.

Partner gets 403 on membership updates:

  • Verify the key includes membership.update.
  • Verify the key is still active.

Last updated: March 28, 2026