Sendy Firmansyah.
← All work

Case study 02 — Ministry of Defense, Indonesia

SIROKUM

Litigation tracking, contracts, correspondence and legal counseling in one Nuxt app — built on a session layer that survives expiring tokens and a permission model that scales by wildcard.

Role

Frontend developer · freelance

Period

2025 — 2026

Client

Ministry of Defense, Indonesia

Stack

Nuxt 4, Vue 3, TypeScript, Pinia, Element Plus, WebSocket, Chart.js, Tailwind CSS

41/47

pages built on one shared layout component

1

token refresh for any number of concurrent 401s

27

pages gated by wildcard permissions

Context

SIROKUM is the legal affairs platform for Indonesia’s Ministry of Defense. It covers litigation and non-litigation cases with hearing schedules and timelines, cooperation agreements and contracts, incoming and outgoing correspondence, legal counseling activities, and performance reporting — each with its own filters, exports and print layouts.

I built the frontend as a freelancer on Nuxt 4 and TypeScript, as the main contributor in a small team.

The hard problem: sessions that expire mid-work

Access tokens are short-lived and renewed through an httpOnly refresh cookie. A dashboard fires many requests at once, so when a token expires, they all fail with 401 together. Handled naively, each one triggers its own refresh — the refreshes race, rotated tokens invalidate each other, and the user is logged out in the middle of filling a form.

What I did

  • Single-flight refresh. One shared promise for the refresh call. Every request that hits a 401 awaits the same promise, which is cleared once it settles. If the refresh itself fails, the session is cleared once.
  • Replay, once. The API wrapper catches the 401, awaits the refresh, then replays the original request once. The refresh endpoint is excluded so a dead session can’t loop.
  • Real-time stays authenticated. The notification WebSocket re-authenticates whenever the token changes, and reconnects with exponential backoff when the connection drops.

There’s a second chapter. In a later audit I found my first version replayed the request inside ofetch’s onResponseError hook. That hook’s return value is ignored — ofetch rethrows the original 401 regardless. The replayed request did reach the server, but its response was thrown away: a form submit could save while the UI showed an error, inviting a duplicate. I moved the replay into the wrapper, deleted a now-redundant request queue, and proved the fix against a local test server: 5 concurrent 401s, 1 refresh, 5 successful responses.

Permissions that scale by wildcard

Access is modeled as resource:action strings with 2 levels of wildcard — * for full access, resource:* for a whole module. One small hasPermission check resolves all three, and it gates page content and individual actions across 27 pages and the shared components. Adding a role is data, not code.

A layout component that removed the boilerplate

Every page in a ministry app wants the same chrome: breadcrumbs 3 levels deep, a back button, year or year-range filters, an export menu and a print action with a choice of signatory. I put all of it behind one MyPage component with typed props and two-way bindings for filters. 41 of the 47 pages use it, so a new module starts at its actual content. Alongside it: a shared table, a family of chart components, and a litigation timeline.

Outcome

Sessions survive expiry invisibly, concurrent requests never stampede the refresh endpoint, and new modules are mostly business logic. The audit chapter is the part I’d tell a team about: the bug was quiet, plausible-looking and only showed up when a token expired mid-request — so now I test retry paths against a real server, not against my assumptions about a library.

Next case study

ELAPKIN