Appearance
API Gateway Contract Notes
Public compatibility base path remains /api.
Current baseline routes:
/healthz/readyz/metrics/api/health/live/api/health/ready/v1/routes/api/*legacy proxy by route table
Route states:
legacy_proxyshadow_readnative_readnative_write_shadow_validatenative_writeremoved
Route Table Fields
json
{
"name": "exam-import-docx-fast-create",
"prefix": "/api/exam-import/docx-fast-jobs",
"methods": ["POST"],
"exact": true,
"state": "native_write",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/docx-fast-jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}methods is optional. When present, only those HTTP methods match. exact is optional and defaults to false; set it to true when only the exact public path should match and subroutes must fall through to a broader legacy route. suffix_segments is optional and defaults to 0; set it to a positive value when a route should match exactly that many path segments after prefix. For example, prefix: "/api/questions" plus suffix_segments: 1 matches /api/questions/{id} but not /api/questions, /api/questions/{id}/edit, or /api/questions/ai-classify/jobs. suffix is optional and adds a literal final-segment guard when suffix_segments is used; for example, suffix: "/approve" keeps sibling actions such as /stop on a broader legacy route. target_prefix rewrites the public legacy-compatible path to the native service path. If it is omitted, the original path is forwarded unchanged.
require_auth, required_roles, and require_organization are optional route-level auth adapter fields. When a route opts in, the gateway verifies the legacy access token from Authorization or the hoctapaz.accessToken cookie using AUTH_JWT_SECRET, enforces any listed roles, derives organization from X-Organization-Id, organizationId, or the token default organization, and forwards X-User-Id, X-User-Role, X-User-Full-Name, optional X-User-Email, and X-Organization-Id to the native service. Omit these fields for public routes and for routes that must remain legacy-authenticated.
A local native-routing example lives at deploy/gateway/routes.native-example.json. It is intentionally not the default route table because the default must preserve current frontend contracts.
Auth session route rehearsal:
json
{
"name": "auth-login",
"prefix": "/api/auth/login",
"methods": ["POST"],
"exact": true,
"state": "native_write",
"target": "http://auth-service:8080",
"target_prefix": "/v1/auth/login"
}The Compose-facing auth rehearsal table is deploy/gateway/routes.auth-native-example.json; the local go run variant is deploy/gateway/routes.auth-native-localhost-example.json. It routes only the Phase 8 native session foundation paths to auth-service:
POST /api/auth/registerPOST /api/auth/loginPOST /api/auth/refreshPOST /api/auth/logoutGET /api/auth/me
These auth routes intentionally omit the gateway auth adapter because auth-service directly handles login/register bodies and bearer/refresh tokens. The broader /api/auth route remains legacy-proxied so Google auth, test-login, forgot/reset password, profile, KYC, and email-change routes do not move with this rehearsal. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default auth route entries.
Run make test-auth-routes to statically validate the default and native auth route tables before any browser session rehearsal.
Organization read route rehearsal:
json
{
"name": "organizations-list",
"prefix": "/api/organizations",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://school-service:8080",
"target_prefix": "/v1/organizations",
"require_auth": true,
"require_organization": true
}The Compose-facing organization read rehearsal table is deploy/gateway/routes.organizations-read-native-example.json; the local go run variant is deploy/gateway/routes.organizations-read-native-localhost-example.json. It routes only read paths from the admin organization surface to school-service:
GET /api/organizationsGET /api/organizations/{organizationId}GET /api/organizations/{organizationId}/unitsGET /api/organizations/{organizationId}/members
These routes use the gateway auth adapter only for identity and organization header injection. They intentionally do not set global required_roles because legacy organization management is controlled by tenant membership roles (OWNER/ORG_ADMIN) rather than only the global JWT role. Organization writes, purge, unit/member mutations, student-import creation, the broad /api/organizations route, and fallback remain legacy-proxied. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default organization route entries.
Run make test-organization-routes to statically validate the default and native organization read route tables before any live rehearsal.
Classroom adapter preflight guard:
The classroom-service Phase 8 foundation is intentionally not exposed through a public /api/classrooms* or /api/admin/classrooms* gateway route table yet. Legacy classroom list/detail/member/student/admin responses hydrate teacher, organization, subject, user, lesson, assignment, attempt, and deferred counter data that the current service-owned classroom rows do not provide. The default route table and all current rehearsal tables must therefore keep classroom public/admin routes on legacy_proxy.
Run make test-classroom-route-guard to statically validate that deploy/gateway/routes.json and non-default route-table examples do not accidentally promote /api/classrooms* or /api/admin/classrooms* to native or shadow states. Future classroom adapters must update this guard only after auth/RBAC, cross-service validation, response hydration, and deferred count contracts are implemented.
Student course read route rehearsal:
json
{
"name": "student-courses-list",
"prefix": "/api/student/courses",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://course-service:8080",
"target_prefix": "/v1/student/courses",
"require_auth": true,
"required_roles": ["STUDENT"],
"require_organization": true
}The Compose-facing student-course read rehearsal table is deploy/gateway/routes.student-courses-read-native-example.json; the local go run variant is deploy/gateway/routes.student-courses-read-native-localhost-example.json. It routes only student course reads to course-service:
GET /api/student/coursesGET /api/student/courses/{courseId}
These routes use the gateway auth adapter for STUDENT role enforcement and organization header injection, matching the legacy TenantGuard surface. Recommendation, purchase, lesson progress, video progress, material view, broad /api/student/courses, broad /api/courses, broad /api/public, and fallback remain legacy-proxied. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default student-course route entries.
Run make test-student-course-routes to statically validate the default and native student-course route tables before any live rehearsal.
Student course progress route rehearsal:
json
{
"name": "student-course-lesson-progress",
"prefix": "/api/student/courses",
"methods": ["POST"],
"suffix_segments": 4,
"suffix": "/progress",
"state": "native_write",
"target": "http://course-service:8080",
"target_prefix": "/v1/student/courses",
"require_auth": true,
"required_roles": ["STUDENT"],
"require_organization": true
}The Compose-facing student-course progress rehearsal table is deploy/gateway/routes.student-course-progress-native-example.json; the local go run variant is deploy/gateway/routes.student-course-progress-native-localhost-example.json. It routes only student-owned progress writes to course-service:
POST /api/student/courses/{courseId}/lessons/{lessonId}/progressPOST /api/student/courses/{courseId}/lessons/{lessonId}/video-progressPOST /api/student/courses/{courseId}/materials/{materialId}/view
These routes use the gateway auth adapter for STUDENT role enforcement and organization header injection. Student course list/detail are covered by the separate P8-008 read table and stay legacy in this progress-only table. Mastery, recommendation, purchase, broad /api/student/courses, broad /api/courses, broad /api/public, and fallback remain legacy-proxied. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default student-course progress route entries.
Run make test-student-course-progress-routes to statically validate the default and native student-course progress route tables before any live rehearsal.
Notification route rehearsal:
json
{
"name": "notifications-list",
"prefix": "/api/notifications",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://notification-service:8080",
"target_prefix": "/v1/notifications",
"require_auth": true
}The Compose-facing notification rehearsal table is deploy/gateway/routes.notifications-native-example.json; the local go run variant is deploy/gateway/routes.notifications-native-localhost-example.json. It routes only inbox and preference paths to notification-service:
GET /api/notificationsGET /api/notifications/{notificationId}POST /api/notifications/{notificationId}/readPOST /api/notifications/{notificationId}/deletePOST /api/notifications/read-allPOST /api/notifications/delete-allGET /api/notifications/preferencesPATCH /api/notifications/preferencesGET /api/alerts/preferencesPATCH /api/alerts/preferences
These routes use the gateway auth adapter for actor header injection but do not require organization at the gateway because the legacy notification inbox is primarily user-scoped and the shell notification bell fetches with only Authorization. Notification create, batch, snapshot, parent alerts, weak-topic alerts, admin inbox, broad /api/notifications, broad /api/alerts, broad /api/admin, and fallback remain legacy-proxied. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default notification route entries.
Run make test-notification-routes to statically validate the default and native notification route tables before any live rehearsal.
Parent alert route rehearsal:
json
{
"name": "parent-alerts-list",
"prefix": "/api/parent/alerts",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://notification-service:8080",
"target_prefix": "/v1/parent/alerts",
"require_auth": true,
"required_roles": ["PARENT"]
}The Compose-facing parent-alert rehearsal table is deploy/gateway/routes.parent-alerts-native-example.json; the local go run variant is deploy/gateway/routes.parent-alerts-native-localhost-example.json. It routes only the parent-facing alert read surface to notification-service:
GET /api/parent/alertsPOST /api/parent/alerts/{notificationId}/readPOST /api/parent/alerts/read-all
These routes use the gateway auth adapter and global PARENT role check so notification-service receives parent actor headers. They do not require organization at the gateway because the legacy parent alert page fetches with only the current parent identity. Weak-topic alert creation, parent-child relationship lookup, broad /api/parent/alerts, broad /api/notifications, broad /api/alerts, broad /api/admin, and fallback remain legacy-proxied. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default parent-alert route entries.
Run make test-parent-alert-routes to statically validate the default and native parent-alert route tables before any live rehearsal.
Feature maintenance route rehearsal:
json
{
"name": "admin-feature-maintenance-list",
"prefix": "/api/admin/feature-maintenance",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://admin-service:8080",
"target_prefix": "/v1/admin/feature-maintenance",
"require_auth": true,
"required_roles": ["ADMIN"]
}The Compose-facing feature-maintenance rehearsal table is deploy/gateway/routes.feature-maintenance-native-example.json; the local go run variant is deploy/gateway/routes.feature-maintenance-native-localhost-example.json. It routes only the native feature-maintenance public/admin endpoints to admin-service:
GET /api/feature-maintenance/publicGET /api/admin/feature-maintenancePATCH /api/admin/feature-maintenance/{key}
The public status route intentionally stays unauthenticated. Admin list/update use the gateway auth adapter and global ADMIN role check so admin-service receives X-User-* actor headers. The rehearsal does not require organization at the gateway because the legacy controller is guarded by global admin role, not tenant membership. Broad /api/feature-maintenance, broad /api/admin, AI classification job reads, admin operations/audit routes, users, AI settings, wallet/support/dashboard routes, broad /api/questions, and fallback remain legacy-proxied. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default feature-maintenance route entries.
Run make test-feature-maintenance-routes to statically validate the default and native feature-maintenance route tables before any live rehearsal.
Admin audit route rehearsal:
json
{
"name": "admin-operations-audit",
"prefix": "/api/admin/operations/audit",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://admin-service:8080",
"target_prefix": "/v1/admin/audit-logs",
"require_auth": true,
"required_roles": ["ADMIN"]
}The Compose-facing admin audit rehearsal table is deploy/gateway/routes.admin-audit-native-example.json; the local go run variant is deploy/gateway/routes.admin-audit-native-localhost-example.json. It routes only GET /api/admin/operations/audit to admin-service.
The route uses the gateway auth adapter and global ADMIN role check, but it does not require organization at the gateway because the legacy operations controller is global-admin guarded. Caller-provided organizationId query or X-Organization-Id header remains available to the native audit list as a filter during rehearsal. Admin operations health, queues, retry/clear commands, metrics, import jobs, outbox, AI settings, feature-maintenance routes not included in this table, broad /api/admin, and fallback remain legacy-proxied. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default admin audit route entry.
Run make test-admin-audit-routes to statically validate the default and native admin audit route tables before any live rehearsal.
Analytics read route rehearsal:
json
{
"name": "analytics-results",
"prefix": "/api/analytics/results",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://analytics-service:8080",
"target_prefix": "/v1/analytics/results",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}The Compose-facing analytics rehearsal table is deploy/gateway/routes.analytics-read-native-example.json; the local go run variant is deploy/gateway/routes.analytics-read-native-localhost-example.json. It routes only the two teacher-facing analytics reads to analytics-service:
GET /api/analytics/resultsGET /api/analytics/weak-topics
Both routes use the gateway auth adapter, global ADMIN/TEACHER role checks, and organization header injection to match the legacy JwtAuthGuard, TenantGuard, and role decorators. Analytics snapshot/event writes, exam/classroom/student/parent analytics, classroom export, broad /api/analytics, broad /api/exams, broad /api/classrooms, broad /api/students, broad /api/parents, and fallback remain legacy-proxied. Those routes need summary path adapters, parent-child access checks, report export formatting, result visibility, and cross-service hydration before cutover. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default analytics route entries.
Run make test-analytics-routes to statically validate the default and native analytics route tables before any live rehearsal.
Storage migration example:
json
{
"name": "storage",
"prefix": "/api/storage",
"state": "native_write",
"target": "http://document-service:8080",
"target_prefix": "/v1/storage"
}The full storage route-table example lives at deploy/gateway/routes.storage-native-example.json. It rewrites frontend calls such as /api/storage/presigned-upload to document-service path /v1/storage/presigned-upload without frontend changes.
Question type migration example:
json
{
"name": "question-types-read",
"prefix": "/api/question-types",
"methods": ["GET"],
"exact": true,
"state": "shadow_read",
"target": "http://question-bank-service:8080",
"target_prefix": "/v1/question-types"
}The Compose-facing question-type shadow route-table example lives at deploy/gateway/routes.question-types-shadow-example.json; the local go run variant is deploy/gateway/routes.question-types-shadow-localhost-example.json. The native rehearsal variants are deploy/gateway/routes.question-types-native-example.json and deploy/gateway/routes.question-types-native-localhost-example.json. Shadow examples match only exact GET /api/question-types; native examples match exact GET plus POST/PATCH/DELETE write methods.
The native rehearsal variants also include:
json
{
"name": "question-types-write",
"prefix": "/api/question-types",
"methods": ["POST", "PATCH", "DELETE"],
"state": "native_write",
"target": "http://question-bank-service:8080",
"target_prefix": "/v1/question-types",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}This non-default write route covers POST /api/question-types, PATCH /api/question-types/:id, DELETE /api/question-types/:id, PATCH /api/question-types/bulk-status, and POST /api/question-types/bulk-delete. The shadow examples remain GET-only, and the default route table stays legacy until browser parity and rollback evidence are complete.
Run make test-question-types-routes to statically validate the default, shadow, and native question-type route tables.
Run QUESTION_TYPES_SELF_TEST=1 make test-question-types-live for the hermetic live-smoke self-test. Against a running gateway with the non-default native question-type route table, run:
bash
QUESTION_TYPES_LIVE_CONFIRM=write-native \
QUESTION_TYPES_AUTHORIZATION='Bearer <token>' \
QUESTION_TYPES_ORGANIZATION_ID=<org-id> \
GATEWAY_BASE_URL=http://localhost:8085 \
make test-question-types-liveThe live smoke verifies /v1/routes, then exercises create/update/bulk archive/bulk delete through /api/question-types and checks the gateway route headers. It does not run during normal validation.
Attempt route rehearsal:
json
{
"name": "exam-attempt-start",
"prefix": "/api/exams",
"methods": ["POST"],
"suffix_segments": 2,
"suffix": "/start",
"state": "native_write",
"target": "http://attempt-service:8080",
"target_prefix": "/v1/exams",
"require_auth": true,
"required_roles": ["STUDENT"],
"require_organization": true
}The Compose-facing attempt rehearsal table is deploy/gateway/routes.attempt-native-example.json; the local go run variant is deploy/gateway/routes.attempt-native-localhost-example.json. It routes legacy-compatible attempt runtime paths to attempt-service:
POST /api/exams/:examId/startGET /api/attempts/:attemptIdPOST /api/attempts/:attemptId/answersPOST /api/attempts/:attemptId/submitPOST /api/attempts/:attemptId/eventsGET /api/attempts/:attemptId/eventsGET /api/attempts/:attemptId/result
POST /api/exams/:examId/start rewrites to the attempt-service compatibility alias /v1/exams/:examId/start, which delegates to the native start behavior. The broader /api/exams and /api/attempts routes remain legacy-proxied so exam authoring, publish, analytics, review-like attempt siblings, and unrelated exam routes do not move with this rehearsal. Rollback is returning GATEWAY_ROUTE_TABLE to deploy/gateway/routes.json or deleting the non-default attempt route entries.
Run make test-attempt-routes to statically validate the default and native attempt route tables before any live exam-room rehearsal.
Question list read shadow example:
json
{
"name": "questions-list",
"prefix": "/api/questions",
"methods": ["GET"],
"exact": true,
"state": "shadow_read",
"target": "http://question-bank-service:8080",
"target_prefix": "/v1/legacy/questions"
}The Compose-facing question-list route-table example lives at deploy/gateway/routes.questions-read-shadow-example.json; the local go run variant is deploy/gateway/routes.questions-read-shadow-localhost-example.json. Shadow reads target the compatibility adapter at /v1/legacy/questions so the best-effort native request exercises the same legacy-shaped projection planned for native-read cutover. Keep the broader /api/questions prefix on legacy_proxy so create/update/delete routes, AI classification subroutes, and question folders/groups continue to hit legacy. Rollback is setting the exact questions-list route to legacy_proxy or deleting it from the route table.
Question detail/editor read shadow example:
json
{
"name": "questions-detail",
"prefix": "/api/questions",
"methods": ["GET"],
"suffix_segments": 1,
"state": "shadow_read",
"target": "http://question-bank-service:8080",
"target_prefix": "/v1/legacy/questions"
}This shadows legacy GET /api/questions/:id and keeps ?view=editor unchanged for the adapter detail endpoint. The one-segment suffix matcher is required because /api/questions also owns nested legacy subroutes such as AI classification jobs. Rollback is setting questions-detail to legacy_proxy or deleting it from the route table.
Question read native rehearsal:
json
{
"name": "questions-list",
"prefix": "/api/questions",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://question-bank-service:8080",
"target_prefix": "/v1/legacy/questions",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}The Compose-facing native rehearsal route table is deploy/gateway/routes.questions-read-native-example.json; the local go run variant is deploy/gateway/routes.questions-read-native-localhost-example.json. It routes only GET /api/questions and one-segment GET /api/questions/:id to the compatibility adapter in question-bank-service, forwards gateway-derived X-User-* and X-Organization-Id headers, and keeps writes plus nested /api/questions/* subroutes on legacy. Rollback is changing GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json or setting questions-list and questions-detail back to legacy_proxy.
Run make test-question-read-routes to statically validate the default, shadow, and native question-read route-table examples before live parity runs.
Run make test-question-read-native with a real access token, matching user/org headers, question-bank-service, and the native route table active to byte-compare gateway native responses with direct /v1/legacy/questions adapter responses.
Run make test-question-read-browser with a real browser session after starting the web app with NEXT_PUBLIC_API_URL pointing at the gateway to verify /teacher/questions observes the questions-list native-read header. Set QUESTION_READ_BROWSER_DETAIL_ID=auto when the native read model has at least one question and editor-detail browser evidence is required. Run make test-question-read-browser-admin with an admin token to verify /admin/questions sends allOrgs=1 through the same native-read route.
AI classification apply adapter example:
json
{
"name": "question-classification-apply",
"prefix": "/api/questions/ai-classify/apply",
"methods": ["PATCH"],
"exact": true,
"state": "native_write",
"target": "http://question-bank-service:8080",
"target_prefix": "/v1/questions/ai-classify/apply",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}This carves out only legacy-compatible PATCH /api/questions/ai-classify/apply and rewrites it to PATCH /v1/questions/ai-classify/apply on question-bank-service. The exact matcher is required so /api/questions/ai-classify/apply/*, job creation/status, suggestions, and SSE classification routes remain on the broader /api/questions legacy proxy route. The example opts into gateway auth header forwarding because the native apply endpoint uses service-owned question scope and audit metadata.
The Compose-facing apply example lives at deploy/gateway/routes.question-classification-apply-native-example.json; the local go run variant is deploy/gateway/routes.question-classification-apply-native-localhost-example.json. Rollback is returning the gateway to the default route table or deleting this single route-table entry.
Static route-table coverage is available with:
bash
make test-question-classification-apply-routesThe coverage validates that the default route table has no active native apply carve-out, the native examples route only exact PATCH /api/questions/ai-classify/apply to question-bank-service with auth/org/role guards, broad /api/questions plus fallback remain legacy-proxied, and the apply route is ordered before broad legacy routes.
AI classification job route guard:
The native ai-classifier-service job foundation remains internal-only while BullMQ worker parity, taxonomy-rich prompt context, gateway RBAC, and browser parity are still deferred. Public POST /api/questions/ai-classify/suggestions, /api/questions/ai-classify/jobs*, job SSE, job errors, and cancel routes must therefore stay on the broad legacy questions route. Run make test-ai-classification-job-route-guard to scan default and non-default route-table examples and fail if any active public route targets /v1/ai-classifier* or promotes AI classification job/suggestion prefixes away from legacy. The guard allows only the exact apply carve-out above because that route targets question-bank-service.
An opt-in live smoke for this non-default write route is available with:
bash
QUESTION_CLASSIFICATION_APPLY_CONFIRM=apply-native \
QUESTION_CLASSIFICATION_APPLY_QUESTION_ID=<native-question-id> \
QUESTION_CLASSIFICATION_APPLY_AUTHORIZATION='Bearer <token>' \
QUESTION_CLASSIFICATION_APPLY_ORGANIZATION_ID=<org-id> \
make test-question-classification-apply-liveThe smoke validates /v1/routes, checks GET /api/questions/ai-classify/jobs?limit=1 still stays on the broad legacy questions route, patches the apply request through the gateway, asserts the question-classification-apply route headers, and verifies the legacy-shaped apply result counts include a successful result for the requested question.
Import approval adapter example:
json
{
"name": "exam-import-approval",
"prefix": "/api/exam-import/jobs",
"methods": ["POST"],
"suffix_segments": 2,
"suffix": "/approve",
"state": "native_write",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}This carves out only legacy-compatible POST /api/exam-import/jobs/{id}/approve and rewrites it to docx-import-service path /v1/import/docx/jobs/{id}/approve. Other /api/exam-import/jobs/* actions, including review save, stop, delete, formula conversion, status, and detail, remain on the broader legacy proxy route. The example is intentionally not in the default route table; the non-default route example opts into the gateway auth/header adapter, while public cutover still waits for browser review verification and full approval parity.
The Compose-facing import approval example lives at deploy/gateway/routes.import-approval-native-example.json; the local go run variant is deploy/gateway/routes.import-approval-native-localhost-example.json. Run make test-import-approval-routes to statically verify the default table keeps approval on legacy and the non-default examples carve out only POST /api/exam-import/jobs/{id}/approve.
Run the live smoke only against a gateway started with one of the non-default approval route tables and native dependencies:
bash
cd go-platform
IMPORT_APPROVAL_LIVE_CONFIRM=approve-native \
IMPORT_APPROVAL_JOB_ID=<completed-job-id> \
IMPORT_APPROVAL_AUTHORIZATION='Bearer <token>' \
IMPORT_APPROVAL_ORGANIZATION_ID=<org-id> \
make test-import-approval-liveThe live smoke checks /v1/routes, verifies sibling job detail still routes to the broad legacy exam-import route, then posts approval through exam-import-approval and validates the native approval result shape. It is intentionally opt-in because it writes approved questions into native question-bank and may create draft exam snapshots when the request target is EXAM_DRAFT.
Import review-save adapter example:
json
{
"name": "exam-import-review-save",
"prefix": "/api/exam-import/jobs",
"methods": ["PATCH"],
"suffix_segments": 2,
"suffix": "/review",
"state": "native_write",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}This carves out only legacy-compatible PATCH /api/exam-import/jobs/{id}/review and rewrites it to PATCH /v1/import/docx/jobs/{id}/review. It preserves reviewed parse payloads verbatim so manual question-type override metadata can survive reload. Source-text reparse and native materialized DOCX Fast token acknowledgement are supported by the native review endpoint. Non-materialized legacy temp-draft store materialization remains on legacy. Rollback is deleting this route or setting it to legacy_proxy.
The Compose-facing review-save example lives at deploy/gateway/routes.import-review-native-example.json; the local go run variant is deploy/gateway/routes.import-review-native-localhost-example.json.
Import detail reload adapter example:
json
{
"name": "exam-import-job-detail",
"prefix": "/api/exam-import/jobs",
"methods": ["GET"],
"suffix_segments": 1,
"state": "native_read",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}This carves out only GET /api/exam-import/jobs/{id} and rewrites it to GET /v1/import/docx/jobs/{id}. The native response includes parseResultJson from the saved reviewed payload when present, so reload can keep manual question-type overrides. Sibling paths such as /status, /review, and /approve stay on legacy unless their own route-table entries are enabled.
The Compose-facing detail example lives at deploy/gateway/routes.import-detail-native-example.json; the local go run variant is deploy/gateway/routes.import-detail-native-localhost-example.json.
Combined import review save/detail smoke example:
json
{
"routes": [
{
"name": "exam-import-review-save",
"prefix": "/api/exam-import/jobs",
"methods": ["PATCH"],
"suffix_segments": 2,
"suffix": "/review",
"state": "native_write",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
},
{
"name": "exam-import-job-detail",
"prefix": "/api/exam-import/jobs",
"methods": ["GET"],
"suffix_segments": 1,
"state": "native_read",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}
]
}Use the combined example only when preparing save-then-reload smoke or browser verification for P4-003. It keeps /api/exam-import/jobs/{id}/status, /approve, /stop, conversion routes, and unrelated import paths on legacy via the broader /api/exam-import proxy. Rollback is returning the gateway to the default route table or removing the two native entries.
The Compose-facing combined route table lives at deploy/gateway/routes.import-review-roundtrip-native-example.json; the local go run variant lives at deploy/gateway/routes.import-review-roundtrip-native-localhost-example.json.
DOCX Fast create-job adapter example:
json
{
"name": "exam-import-docx-fast-create",
"prefix": "/api/exam-import/docx-fast-jobs",
"methods": ["POST"],
"exact": true,
"state": "native_write",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/docx-fast-jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}This carves out only legacy-compatible POST /api/exam-import/docx-fast-jobs. The native endpoint accepts the legacy JSON body with questionStorageKey, fetches uploaded DOCX bytes through document-service, and returns the legacy success envelope expected by the frontend API helper. The Compose-facing route table lives at deploy/gateway/routes.import-create-native-example.json; the local go run variant is deploy/gateway/routes.import-create-native-localhost-example.json. Static coverage for this route table is available with make test-import-create-routes.
An opt-in live smoke for the same non-default route table is available with:
bash
IMPORT_CREATE_LIVE_CONFIRM=create-native \
IMPORT_CREATE_LIVE_STORAGE_KEY=<uploaded-docx-storage-key> \
IMPORT_CREATE_LIVE_AUTHORIZATION='Bearer <token>' \
IMPORT_CREATE_LIVE_ORGANIZATION_ID=<org-id> \
make test-import-create-liveThe smoke validates /v1/routes, posts the legacy-compatible DOCX Fast create body through the gateway, asserts the exam-import-docx-fast-create route headers, verifies the legacy success envelope, and checks a sibling job detail request still stays on the broad legacy import route. It does not run during normal tests and does not promote the default route table.
Browser-route evidence for the real import surface is available with:
bash
IMPORT_CREATE_BROWSER_AUTHORIZATION='Bearer <token>' \
IMPORT_CREATE_BROWSER_ORGANIZATION_ID=<org-id> \
IMPORT_CREATE_BROWSER_DOCX_FILE=/path/to/sample.docx \
make test-import-create-browserIf the DOCX object already exists, use IMPORT_CREATE_BROWSER_STORAGE_KEY=<uploaded-docx-storage-key> instead of IMPORT_CREATE_BROWSER_DOCX_FILE. The browser smoke opens the import UI, captures the browser-observed POST /api/exam-import/docx-fast-jobs response, and verifies the same route headers and legacy envelope.
The same non-default table can also carve out the materialized DOCX Fast temp-draft read branch:
json
{
"name": "exam-import-docx-fast-temp-draft",
"prefix": "/api/exam-import/docx-fast-jobs",
"methods": ["GET"],
"suffix_segments": 2,
"suffix": "/temp-draft",
"state": "native_read",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/docx-fast-jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}Read-only live smoke coverage for this non-default route is available with:
bash
IMPORT_TEMP_DRAFT_JOB_ID=<completed-native-docx-fast-job-id> \
IMPORT_TEMP_DRAFT_AUTHORIZATION='Bearer <token>' \
IMPORT_TEMP_DRAFT_ORGANIZATION_ID=<org-id> \
make test-import-temp-draft-liveThe smoke validates /v1/routes, checks a sibling temp-draft asset metadata request still stays on the broad legacy import route, reads the temp-draft through the gateway, asserts the exam-import-docx-fast-temp-draft route headers, and verifies the legacy materialized draft envelope with token materialized-{id}.
The materialized asset content adapter can be carved out independently. It matches only GET /api/exam-import/docx-fast-jobs/{id}/temp-draft/assets/{asset}/content and leaves other temp-draft asset actions on legacy:
json
{
"name": "exam-import-docx-fast-asset-content",
"prefix": "/api/exam-import/docx-fast-jobs",
"methods": ["GET"],
"suffix_segments": 5,
"suffix": "/content",
"state": "native_read",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/docx-fast-jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}Read-only live smoke coverage for this binary route is available with:
bash
IMPORT_TEMP_ASSET_JOB_ID=<completed-native-docx-fast-job-id> \
IMPORT_TEMP_ASSET_ID=<materialized-temp-asset-id> \
IMPORT_TEMP_ASSET_AUTHORIZATION='Bearer <token>' \
IMPORT_TEMP_ASSET_ORGANIZATION_ID=<org-id> \
make test-import-temp-asset-liveThe smoke validates /v1/routes, checks a sibling temp-draft asset metadata request still stays on the broad legacy import route, streams the asset content through the gateway, asserts the exam-import-docx-fast-asset-content route headers, and verifies the binary response has a non-empty body, Content-Type, and private cache header.
It can also carve out DOCX Fast reprocess while preserving the same job id:
json
{
"name": "exam-import-docx-fast-reprocess",
"prefix": "/api/exam-import/docx-fast-jobs",
"methods": ["POST"],
"suffix_segments": 2,
"suffix": "/reprocess",
"state": "native_write",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/docx-fast-jobs",
"require_auth": true,
"required_roles": ["ADMIN", "TEACHER"],
"require_organization": true
}An opt-in live smoke for this non-default write route is available with:
bash
IMPORT_REPROCESS_CONFIRM=reprocess-native \
IMPORT_REPROCESS_JOB_ID=<native-docx-fast-job-id> \
IMPORT_REPROCESS_AUTHORIZATION='Bearer <token>' \
IMPORT_REPROCESS_ORGANIZATION_ID=<org-id> \
make test-import-reprocess-liveThe smoke validates /v1/routes, asserts the sibling job detail route stays on the broad legacy import route, posts the reprocess request through the gateway, checks the exam-import-docx-fast-reprocess route headers, and verifies the legacy success envelope keeps the same job id and message DOCX Fast import reprocessed.
Import status/history adapter examples:
json
{
"name": "exam-import-teacher-library",
"prefix": "/api/exam-import/teacher-library",
"methods": ["GET"],
"exact": true,
"state": "native_read",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/teacher-library",
"require_auth": true,
"require_organization": true
}json
{
"name": "exam-import-job-status",
"prefix": "/api/exam-import/jobs",
"methods": ["GET"],
"suffix_segments": 2,
"suffix": "/status",
"state": "native_read",
"target": "http://docx-import-service:8080",
"target_prefix": "/v1/import/docx/jobs",
"require_auth": true,
"require_organization": true
}The status/history example also includes exact routing for GET /api/exam-import/algorithm-jobs/events and the older GET /api/imports/{jobId}/status alias. Broader /api/exam-import* and /api/imports* traffic remains on legacy so review save, detail, delete, conversion, and other sibling actions are not captured. The Compose-facing route table lives at deploy/gateway/routes.import-status-native-example.json; the local go run variant is deploy/gateway/routes.import-status-native-localhost-example.json. These non-default examples opt into gateway auth/header injection so native status handlers receive the current user and organization headers. Static coverage for these route tables is available with make test-import-status-routes. For BullMQ bridge rehearsals, make test-docx-bullmq-status fetches GET /api/exam-import/teacher-library?q=<jobId> through this route table and asserts the matching row includes the legacy-shaped queue object. It is a read-only live smoke and does not promote the default route table. Browser route evidence uses make test-docx-import-library-browser, which opens the real /teacher/exams/library page and verifies the browser-observed route headers for the same teacher-library carve-out.
Shadow Read
For GET and HEAD routes in shadow_read, the gateway returns the legacy response and sends a best-effort request to the native target. It logs status matches/diffs without changing the client response.