Skip to content

Technical — OPA Data ​

RAGA's policy data synchronization service, built on Bun + Express 5. It pulls user-role and role-grant data from user-service, merges it into a shared JSON file, then pushes that data to OPA (Open Policy Agent) via its data API. Called by user-service whenever a role or policy changes.

Repository ​

KeyValue
Git Remotehttps://git.tlab.co.id/tarantula/tarantula-v2/opa-bun/opa-data.git
Active Branchmain
bash
git clone https://git.tlab.co.id/tarantula/tarantula-v2/opa-bun/opa-data.git
cd opa-data

Tech Stack ​

LayerTechnology
RuntimeBun 1
FrameworkExpress 5 (TypeScript)
HTTP ClientAxios
File I/OBun native (file, write)
Secret ManagementInfisical SDK v5
LoggingMorgan (HTTP request log)
Buildbun build --compile → single binary

Environment Variables ​

There is no .env file — all variables are injected as container env vars.

Bootstrap (container env vars) ​

VariableDefaultDescription
NODE_PORT3666Express server port
INFISICAL_SITE_URLhttps://app.infisical.comInfisical server URL
INFISICAL_CLIENT_ID—Universal Auth Client ID
INFISICAL_CLIENT_SECRET—Universal Auth Client Secret
INFISICAL_ENVIRONMENT—Target environment (dev / staging / prod)
INFISICAL_PROJECT_ID—Infisical project ID
INFISICAL_PATH—Secrets path in Infisical

Secrets via Infisical ​

VariableDescription
URL_USERuser-service base URL; used to fetch /open-api/role-users and /open-api/role-grants
URL_OPAOPA server base URL; used to PUT data to {URL_OPA}/v1/data

Endpoints ​

MethodPathDescription
GET/role-usersSyncs the user → role mapping from user-service to OPA
GET/role-grantsSyncs the role → policy grants mapping from user-service to OPA
GET/healthHealth check → { status: "UP" }

If the response from user-service isn't a 200, opa-data replies 403 { info: "Conflict in timetable (blocked by OPA)" } (that message is from the code as-is, not an actual OPA conflict). If an exception occurs (e.g. user-service or OPA is unreachable), opa-data replies 500 { info: "Internal Server Error" }.

Sync Flow ​

Data Structure in OPA ​

The data pushed to OPA (PUT /v1/data) is JSON shaped as:

json
{
  "user_roles": {
    "john_doe": ["admin"],
    "jane_smith": ["viewer"]
  },
  "role_grants": {
    "admin": [
      { "id": 1, "name": "read", "label": "Read Workspace", "method": "GET", "path": "/workspace", "status": true },
      { "id": 2, "name": "write", "label": "Write Workspace", "method": "POST", "path": "/workspace", "status": true }
    ],
    "viewer": [
      { "id": 1, "name": "read", "label": "Read Workspace", "method": "GET", "path": "/workspace", "status": true }
    ]
  }
}

Each grant in role_grants[role] isn't transformed by opa-data — it's copied as-is from the policies field of user-service's GET /open-api/role-grants response (id, name, label, method, path, status). The method, path, and status fields are the ones actually consumed by the Rego policy in OPA Config (grant.method, grant.path with * wildcards, grant.status == true); id, name, and label just come along as reference metadata. The PUT /v1/data request to OPA is sent with header Content-Type: application/json-patch+json.

This data is used by the Rego policy in OPA Config to make authorization decisions, which are then consumed by OPA Auth.

The /share/data.json File ​

This file lives on a shared volume between the opa-data container and OPA. On every sync:

  1. The file is read (created fresh with {} if missing)
  2. New data is merged (Object.assign) into the existing data
  3. The file is rewritten
  4. The same file content is PUT to the OPA data API

Position in the Architecture ​

Build & Run ​

bash
# Install dependencies
bun install

# Run development
bun run index.ts

# Build a single binary (used in Docker)
bun build --compile --minify --sourcemap --target=bun-linux-x64 ./index.ts --outfile opa-data

# Run the binary
./opa-data

Docker ​

bash
docker build -t opa-data .
docker run -p 3666:3666 \
  -v /host/share:/share \
  -e INFISICAL_SITE_URL=http://... \
  -e INFISICAL_CLIENT_ID=... \
  -e INFISICAL_CLIENT_SECRET=... \
  -e INFISICAL_ENVIRONMENT=dev \
  -e INFISICAL_PROJECT_ID=... \
  -e INFISICAL_PATH=... \
  opa-data

The -v /host/share:/share volume is required so the data.json file can be read by the OPA container.