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_IDUniversal Auth Client ID
INFISICAL_CLIENT_SECRETUniversal Auth Client Secret
INFISICAL_ENVIRONMENTTarget environment (dev / staging / prod)
INFISICAL_PROJECT_IDInfisical project ID
INFISICAL_PATHSecrets 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.