think
16px
820px

Production Integration Readiness — PP Rebuild · Apostille Rebuild · SABH

Date: 2026-07-13 · Scope: feasibility + difficulty of embedding ahu-ocr-tidyup into the three production systems via: button in prod system → redirect to our app (identity + context) → upload/extract/verify here → push finalized data back to the origin system.

All claims below are code-verified (file:line evidence gathered from the four codebases on this machine).


TL;DR matrix

PP Rebuild Apostille Rebuild SABH (legacy)
Stack NestJS 11 + Prisma 6 + React 18 SPA NestJS 11 + Prisma 6 + React 18 SPA Yii 1.1.13 · PHP 5.6 · server-rendered
Auth Keycloak SSO (RS256/JWKS, sso-dev.kemenkum.go.id, realm public) Same Keycloak SSO Yii session cookie; no SSO; HS256 JWT precedent for S2S only
Button + identity handoff LOW LOW LOW–MEDIUM (token mint must be built)
Data push-back MEDIUM–HIGH MEDIUM–HIGH HIGH
Existing S2S auth to copy ApiKeyGuard (static INTERNAL_API_KEY) None (must be created) /service/* HS256 JWT (sk_epicenter)
Push-back target state Draft / Menunggu Konfirmasi Permohonan draft (then user/verifikator completes there) none clean — must call their finalize pipeline
Overall MEDIUM MEDIUM HIGH

Our own system is unusually ready: multi-issuer token verifier interface, transactional outbox + drain worker, deep-linkable flow routes, and per-flow external-record anchors all already exist — most as deliberately dormant capability. The real build is ~4 well-scoped pieces (§4).


1. The two rebuilds are the same integration, twice

Both PP and Apostille rebuilds are modules of the same AHU portal ("Marina"): same Keycloak realm, same userSession domain cookie carrying the RS256 access token, same gateway (elayanan.ahu.go.id/api/v1/<module> prod, portal-dev.ahu.go.id dev), same React-SPA-per-module layout, same NestJS+Prisma backend shape.

Identity handoff is nearly free. Our backend verifies the same Keycloak token against the same JWKS endpoint — no shared secret, no coordination beyond CORS/redirect URLs. Claims already carry djahu_userId, username, roles (jwt.strategy.ts in both APIs).

sequenceDiagram autonumber participant U as User (browser) participant P as PP/Apostille SPA participant O as Our OCR app participant K as Keycloak (JWKS) participant API as Origin API U->>P: logged in (userSession cookie = Keycloak JWT) P->>O: button → redirect with token (or shared-domain cookie) O->>K: verify RS256 signature via JWKS (stateless) O->>O: POST /api/auth/sso → mint OUR session JWT (role-mapped) Note over O: upload → OCR/LLM extract → rules → verifikator approves (days, our session) O->>API: push-back: create permohonan in DRAFT via S2S credential API->>API: origin's own pipeline: numbering, billing, notifications O->>U: "Kembali ke PP/Apostille" (returnUrl)

Two caveats that shape the design:

  • Token lifetime ≈ 15 min. The handoff token authenticates arrival only. We exchange it for our own session immediately (endpoint to build: POST /api/auth/sso). The multi-day verification runs on our session.
  • Push-back cannot ride the user's token (long expired by completion). It needs a machine credential — and neither rebuild's JWT strategy tolerates Keycloak service-account tokens today (both dereference resource_access.djahu.roles; a client-credentials token without that claim throws). So push-back auth = their ApiKeyGuard pattern (PP has it; Apostille must add one) or a Keycloak client whose tokens carry the expected claims.

1a. PP Rebuild specifics

  • Button: sidebar menu is API-driven with a hardcoded fallback + LABEL_URL_MAP (PerseroanPerorangan.jsx:159-207); module_code: "-" renders for everyone. Their FE env is baked at build time → needs their rebuild/redeploy.
  • Inbound surface: full transaction controllers exist (POST /pendirian|perubahan|pembubaran|perbaikan, PATCH .../verify/:id) but all user-JWT-bound with ownership checks (pendirian.repository.ts:2486). ApiKeyGuard (api-key.guard.ts:15-27, single static INTERNAL_API_KEY) already protects S2S endpoints for Fidusia/DJP/Kemensos — the template for a new push-back endpoint.
  • Side effects live server-side and must not be forked: verifyPendirian/finalizePendirian allocate no_permohonan/SK/sertifikat numbers under pg advisory locks, insert the m_ptp master row, queue PDF generation, register real NPWP at DJP CTAS, push pemilik-manfaat to the portal, send emails. Push into Draft/Menunggu Konfirmasi Permohonan and let their pipeline finalize.
  • Traps: PNBP voucher validation is frontend-only in the rebuild (backend stores id_voucher unvalidated) — a push-back endpoint must add server-side voucher verification or it's a fee bypass. One-pendirian-per-account rule (GET /pendirian/cek-eligibility) — we should call it before accepting uploads. Post-verify queue reuses the user's bearer token for the portal PM push — another reason finalization should stay in their UI.
  • Found in passing (flag to their team): real credentials committed in docker-compose.yaml; POST /pendirian/generate-npwp-by-ptp/:id_ptp is @Public() with no guard.

1b. Apostille Rebuild specifics

  • Button: RedirectExternal.js precedent exists; dashboard button trivial, proper menu item = portal menu DB record + LABEL_URL_MAP + rebuild.
  • Inbound surface: exactly the right endpoints exist — POST /permohonan (accepts draft|dikirim), POST /permohonan/upload (multipart, ownership-checked tmp-file flow), PATCH /permohonan/:id. No S2S auth at all today; SIMPADHU is outbound-only (no inbound callback precedent). The is_superapp flag is the precedent for tagging an external channel (schema.prisma L71) — add an is_ocr-style source field.
  • Side effects: submitting dikirim fires the whole chain for free (numbering, Verifikator+Kasi rows, least-loaded auto-assign, QR surat pengantar, notifications, email). Kasi-approve generates PNBP vouchers with the verifying user's token — so if our verifikators replace theirs, we inherit an impersonation problem. Pre-fill draft is by far the lowest-risk pattern (zero side effects; their users/verifikators complete in Apostille).
  • Policy flag: our schema records an explicit decision "does NOT submit back to Apostille — read-only integration only (2026-06-23)" (schema.prisma:81-85). This initiative reverses that — should be an explicit, recorded re-decision.
  • No idempotency on their create endpoint — duplicate pushes = duplicate permohonan; our outbox's idempotency key protects our side, but their endpoint should also be made replay-safe.

2. SABH (legacy) — the hard one

  • Button: easy. One CHtml::link in e.g. views/persero/pembubaran/list.php:24 or the detail_transaksi_button.php strip; menu is DB-driven. Context is available (session['detail_perseroan'] → id_perseroan/nama/SK; Yii::app()->user->profilid, groups).
  • Identity handoff: must be built, but has a template. No SSO exists. ServiceController::actiongetToken (ServiceController.php:690-753) already mints HS256 JWTs (secret sk_epicenter) for external systems — copy that pattern to mint a short-lived user-handoff token in the button controller. Our multi-issuer verifier consumes it like any other issuer.
  • Push-back: HIGH. There is a real, JWT-authenticated external API (/service/*, ~150 actions, with write precedent) — but no action performs a correct PT transaction. The existing sabh/api/PermohonanBerakhirnyaStatusBadanHukum "create" endpoint is a data-capture stub: writes one orphan table, skips everything. A faithful push-back must run, in order, across ~12 MySQL databases with no cross-DB transaction:
    1. SIMPADHU voucher consumption (TERPAKAI 0→1, BillingSimponi.php:1551-1581)
    2. SK/SP numbering + unique_bakum_nomor reservation (TransaksiPerseroan::pushPerubahan:3316-3567)
    3. Master tbl_perseroan update (correct status_transaksi), year-sharded tbl_transaksi_perseroan_YYYY rows (auto-CREATE TABLE LIKE)
    4. The right verifikasi worklist rows (L1→L3 state machine) — or an explicit decision that our verification replaces theirs
    5. nomor_daftar for Berita Negara, BO push (BoServices::sendDataBO), email queue microservice
  • Only sane shape: a new, thin /service/* action inside SABH that internally calls the existing TransaksiPerseroan pipeline — never external writes to their tables. That's PHP 5.6/Yii-1 development + staging verification, in a 514 KB-controller codebase. This is where the timeline risk lives.
  • Found in passing (flag): CSRF globally disabled; VoucherController is hardwired to return "valid" (real check dead-coded); non-JWT /service actions gated by a guessable 30-minute time hash; hardcoded DB creds in config/main.php.

3. Our side — what already exists vs. what to build

Already built (mostly dormant by design):

Capability Where State
Multi-issuer token verifier seam security/token-verifier.ts (interface + "SabhSsoVerifier drops in later, selected by iss" header) designed-for, not implemented
Session minting signLocalToken() token-verifier.ts:34-48 works today
Role-gated route policies + owner scoping security/route-policies.ts, owner.ts, SECURITY_ENFORCE off/shadow/on shadow mode; on implemented
Transactional outbox + drain worker persistence/outbox.ts, outbox-drain.ts, SubmissionOutbox model built, tested, zero production callers
Deep-linkable flow entries /pp/*, /klasifikasi, /apostille/:applicationType?token= routes exist; apostille FE never forwards ?token=
External-record anchors PT: selectedNomorTransaksi+oldData.id_perseroan; PP: oldData.idPtp; Apostille: token only (no permohonan id) flow-specific JSON, no first-class columns
Read-side integration pattern 4 direct-DB read-only clients (SABH, PP registry, Apostille registry, SIMPADHU) settled pattern; no REST client to any origin exists

To build (the honest gap list):
1. SSO-in: per-origin verifiers (Keycloak JWKS for the rebuilds; SABH HS256) + POST /api/auth/sso exchange + claims→persona mapping + FE /sso landing route.
2. Provenance: originSystem / originTransactionRef / returnUrl columns on Submission, stamped at creation; "Kembali ke …" on completion pages.
3. Completion fan-in: one service unifying our three terminal state machines (PT verifikator SELESAI, Apostille decide, PP konfirmasi) → enqueueOutbox inside those transactions (or finally wire the dead hooks.onFinalize).
4. Push clients: an OutboxSender per origin + per-flow payload builders; boot startOutboxDrainLoop; admin ops surface for FAILED rows.
5. Hardening at flip: SECURITY_ENFORCE=on, CORS pinned to the three origins, per-origin secrets in env.

4. Suggested phasing + effort (for timeline estimation)

Effort in focused dev-weeks (1 person + AI assist); calendar time will be dominated by cross-team coordination (portal menu records, Keycloak client/claims, CORS allowlists, S2S credential issuance, SABH staging access) — treat every "their side" item as a dependency to start early.

Phase Work Ours Theirs Wall-clock risk
F. Foundation (serves all 3) SSO-in exchange + FE landing; provenance columns + return UX; outbox wiring + admin surface 3–4 wk low
1. PP Rebuild Payload builders + OutboxSender; call their eligibility check 2–3 wk button + menu entry (~days) + new ApiKey push-back endpoint delegating to existing repo methods incl. server-side voucher check (1–2 wk) medium — their FE rebuild + endpoint
2. Apostille Rebuild Sender + payload (draft pre-fill); store origin permohonan id 2–3 wk button (~days) + S2S auth from scratch + is_ocr flag + idempotency (1–2 wk) medium + policy re-decision (reverses 2026-06-23 read-only rule; verification-ownership question)
3. SABH Sender + payload per flow (start: pembubaran/berakhirnya only) 2–3 wk button + handoff-token mint (~1 wk); new /service/* action invoking TransaksiPerseroan pipeline: 4–8 wk incl. staging verification high — 12-DB side effects, PHP 5.6, no cross-DB atomicity

Rough totals (sequential, one main dev): foundation + PP ≈ 6–9 wk; + Apostille ≈ 10–14 wk; + SABH ≈ 16–24 wk. With the 3-dev sandbox running in parallel after the foundation phase, PP and Apostille can overlap; SABH's critical path is their-side PHP work regardless of our parallelism.

Sequencing recommendation: matches your existing priority (PP → Apostille → PT/SABH) and the difficulty gradient — PP first is also the cheapest proof of the whole loop.

5. Key design decisions to lock early

  1. Push-back lands in a pre-final state (Draft/Menunggu Konfirmasi/draft) and the origin's own pipeline finalizes — vs. full finalization from outside. Strongly recommend pre-final everywhere; for SABH it's the only defensible option, via their own code path.
  2. Whose verifikator is authoritative? If ours replaces theirs (Apostille Verifikator→Kasi, SABH L1→L3), the origin systems need to accept externally-verified transactions — an org/policy question, not a code one. If theirs re-verify, we're a pre-fill accelerator (weaker value prop, much lower risk). This decision changes the Apostille/SABH push-back design materially.
  3. Machine identity per origin: PP = shared INTERNAL_API_KEY (ask for a per-client key), Apostille = to be created, SABH = sk_epicenter JWT. Who issues/rotates these — and does Kemenkum's Keycloak team give us a client?
  4. Apostille write-back reverses our recorded 2026-06-23 read-only decision — re-decide explicitly.

6. Security findings to relay to system owners (found in passing)

  • PP: live credentials committed in docker-compose.yaml; unauthenticated @Public() NPWP-generation endpoint; backend trusts client-side voucher validation.
  • Apostille: no M2M auth story; no idempotency on permohonan create.
  • SABH: CSRF disabled globally; VoucherController hardwired "valid"; time-hash-gated /service actions; hardcoded DB credentials.

These are their systems' issues, not ours — but our integration inherits several (esp. the static API key and voucher gaps), so worth raising when we ask for the S2S credentials.


7. Locked plan — authoritative model (2026-07-13)

Decisions locked (supersede §4–§5 where they differ):
- We remain authoritative — our verifikators perform the verification, and we push the final verified state back to the origin system.
- We may commission new endpoints in PP Rebuild, Apostille Rebuild, and SABH.

7.1 "Machine credentials" (why they're now mandatory)

Push-back lands days after the user left — the handoff token is dead, no human is in the loop. So the call from our backend authenticates as the OCR service (machine credential), and the payload carries the explicit subject (origin user / permohonan id / id_perseroan) + idempotency key. Forms: static API key (PP has this), OAuth2 client-credentials = Keycloak service account (proper; one can serve both rebuilds), HMAC signing (hardening), mTLS (heaviest). Pattern = service-account + explicit subject (avoids Keycloak impersonation/token-exchange).

Per origin: SABH → service account → reuse ServiceController::getToken HS256/sk_epicenter JWT (already a proven S2S pattern). PP → dedicated API key (not the shared INTERNAL_API_KEY) or KC service account. Apostille → Keycloak service account (ideally the same one as PP).

7.2 What each new endpoint must do

  1. Machine-auth + explicit subject + idempotency key (outbox retries).
  2. Trust our verdict → skip their manual verifikator review, but still run their finalization through their own code: numbering, PNBP/voucher consumption, SK/certificate, notifications, Berita Negara, BO push. We never replicate these.
  3. Store our verification attestation (who/when/decision).
  4. Return the official reference (permohonan/SK number) so we round-trip it onto our Submission.

SABH's is the hard one only because "run finalization" there = orchestrating ~12 MySQL DBs with no cross-DB transaction — which is exactly why they build it (calling TransaksiPerseroan), never us.

7.3 Updated timeline

Assumes 1 main dev our side + their teams staffed + SABH endpoint started in parallel from week 1.

Work Our side Their side First live (from kickoff)
Contract spec (unblocks all) ~1 wk wk 1
Foundation (SSO-in, provenance, outbox wiring, sender framework) 3–4 wk
PP adapter 1.5–2.5 wk new authoritative endpoint 2–3 wk + button/CORS (days) ~wk 8 (~2 mo)
Apostille adapter 1.5–2.5 wk endpoint + S2S auth from scratch 2–4 wk ~wk 10–12
SABH adapter 2–3 wk /service/*TransaksiPerseroan finalize 5–8 wk + staging ~wk 12–16

First system (PP) ≈ 2 months; all three ≈ 4 months (16 wk) if SABH runs in parallel now — else 5–6 months (20–24 wk) sequential. Biggest calendar lever: freeze the contract spec early + start SABH's endpoint on day 1.

7.4 Deliverables vs. asks

Ours (in order): (1) contract spec — first, not last; (2) foundation, dormant until env-flip; (3) per-origin adapters (PP → Apostille reuses ~80% → SABH).

Request from their teams (front-load — this is the calendar): a new authoritative push-back endpoint each (per spec); credentials (SABH service account, PP dedicated key, Apostille KC service account); button + CORS + portal menu record on the rebuilds; SABH staging access + named champion starting week 1.

Immediate next artifacts: the integration contract spec + three ready-to-send endpoint-request briefs (one per team) — the things that actually unblock parallel work.