Skip to content

Configuration Guide

Registry Server provides rich configuration options that can be customized through environment variables and configuration files.

Environment Variable Configuration

Create Configuration File

Create a .env file in the apps/registry-server directory:

bash
cd apps/registry-server
cp .env.example .env

Basic Configuration

VariableDefaultDescription
PORT8080Server port
HOST0.0.0.0Server bind address
NODE_ENVdevelopmentRuntime environment
LOG_LEVELinfoLog level
TRUST_PROXYfalseWhether to trust X-Forwarded-For. Accepts true / false / positive hop count. Required behind reverse proxies for per-client rate limiting (§6.19).

Example Configuration

bash
# .env
PORT=8080
HOST=0.0.0.0
NODE_ENV=production
LOG_LEVEL=info
# Behind Nginx / ALB / Cloudflare Tunnel: follow X-Forwarded-For
TRUST_PROXY=true

Listen Address

  • 0.0.0.0 - Listen on all network interfaces (suitable for server deployment)
  • 127.0.0.1 - Local access only (suitable for development)

Storage Configuration

VariableDefaultDescription
STORAGE_ROOT../../packages/storageStatic resource root directory (relative path)
STORAGE_BACKENDlocalUpload storage backend: local or r2
R2_BUCKET_NAMER2 bucket name (required when STORAGE_BACKEND=r2)
R2_ACCOUNT_IDCloudflare account ID (required when STORAGE_BACKEND=r2)
R2_ACCESS_KEY_IDR2 API access key ID (required when STORAGE_BACKEND=r2)
R2_SECRET_ACCESS_KEYR2 API secret access key (required when STORAGE_BACKEND=r2)

Example Configuration (Local)

bash
# Using relative path
STORAGE_ROOT=../../packages/storage

# Using absolute path (recommended for production)
STORAGE_ROOT=/data/registry-storage

Example Configuration (R2)

bash
STORAGE_BACKEND=r2
R2_BUCKET_NAME=rack-registry
R2_ACCOUNT_ID=your-account-id
R2_ACCESS_KEY_ID=your-access-key-id
R2_SECRET_ACCESS_KEY=your-secret-access-key

Storage Backend

When STORAGE_BACKEND=local (default), uploaded packages are stored on the local filesystem under STORAGE_ROOT and the Server handles both reads and writes. When STORAGE_BACKEND=r2, uploaded packages are pushed to a Cloudflare R2 bucket and reads are served by a Cloudflare Worker directly from R2 at the edge (registry.rackjs.com or your own Worker domain). In both modes, upload processing (temp files, checksum verification, tar extraction) happens locally — only the final storage destination differs.

⚠️ In r2 mode the Server no longer writes to local disk. Its GET /registries/** routes still exist but read from an empty local directory, so hitting the Server for downloads returns 404. Clients (Rack CLI, browser, CI) must point at the Worker domain for reads; only POST /registries uploads go to the Server. See Deployment Overview for the full topology.

Path Specification

  • Relative path: Relative to the apps/registry-server directory
  • Absolute path: Recommended for production to avoid path confusion

Authentication Configuration

VariableDefaultDescription
AUTH_CONFIG_PATH../../config/auth.jsonPath to auth.json (repo-root config/auth.json, shared with the Worker)
ADMIN_TOKEN(not set)System-level admin token; bypasses namespace auth for reads and uploads

Example Configuration

bash
# Authentication configuration (defaults to repo-root config/auth.json, shared with the Worker)
# AUTH_CONFIG_PATH=../../config/auth.json

# Admin token (optional, enables cross-namespace reads and publishes)
ADMIN_TOKEN=your-secret-admin-token

Admin Token

ADMIN_TOKEN is a system-level master key. Holders may read (GET /registries/*, GET /namespaces) and publish to any namespace without per-namespace token configuration — when a request carries this token, both upload and read paths skip namespace-level auth checks. Useful for CI/CD systems that operate across multiple namespaces.

R2 mode requires auth.json to be synced to R2

In STORAGE_BACKEND=r2 mode, the Cloudflare Worker reads .auth/auth.json from the same R2 bucket to authenticate read requests, while the Server still reads the repo-root file to gate uploads. The sync-auth.yml workflow uploads config/auth.json to R2 on every push — without this sync (or a manual upload to .auth/auth.json), the Worker returns 403 for every namespace read.

Webhook Configuration

VariableDefaultDescription
WEBHOOK_CONFIG_PATHconfig/webhooks.jsonPath to webhooks.json configuration file

Complete Example

bash
# apps/registry-server/.env

# Basic configuration
PORT=8080
HOST=0.0.0.0
NODE_ENV=production
LOG_LEVEL=info

# Storage configuration
STORAGE_ROOT=/data/registry-storage
# STORAGE_BACKEND=local

# R2 configuration (required when STORAGE_BACKEND=r2)
# R2_BUCKET_NAME=rack-registry
# R2_ACCOUNT_ID=your-account-id
# R2_ACCESS_KEY_ID=your-access-key-id
# R2_SECRET_ACCESS_KEY=your-secret-access-key

# Authentication configuration (defaults to repo-root config/auth.json, shared with the Worker)
# AUTH_CONFIG_PATH=../../config/auth.json
# ADMIN_TOKEN=your-secret-admin-token

# Webhook configuration
WEBHOOK_CONFIG_PATH=config/webhooks.json

Built-in Defaults (Not Configurable)

The following values are compiled into the server and cannot be changed via environment variables:

SettingValueDescription
Cache-ControlPer-route tieredSee Operations — Response Caching
CompressionAlways enabledSupports gzip, deflate, br encodings
Rate limit max1200 requestsMaximum requests per window
Rate limit window1 minuteRate limit time window
Max upload size100 MBMaximum file upload size

Rate Limiting

Rate limits are applied per client IP. When deploying behind a reverse proxy (Nginx / ALB / Cloudflare Tunnel / …), forwarding X-Forwarded-For alone is not enough — also set TRUST_PROXY=true (or a hop count like 1 / 2) on the server so Fastify follows the chain; otherwise every real client collapses into the proxy's IP and shares the same 1200/min bucket.

When the limit is exceeded, the server returns 429 Too Many Requests with the standard Rack error body:

json
{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Rate limit exceeded. Try again in 30s"
}

Authentication Configuration

The authentication configuration file (repo-root config/auth.json, shared with the Cloudflare Worker) is the single source of truth for namespace access:

  • A namespace must exist as a top-level key; namespaces missing from auth.json always return 403 Forbidden and are hidden from GET /namespaces listings.
  • An empty array [] → anonymous read access (uploads still rejected unless using an admin token). These namespaces are visible to everyone in discovery endpoints.
  • A non-array value (null, string, etc.) or a non-empty array whose entries all lack a valid token → that namespace fails per-namespace validation and is excluded from the allowed-namespaces set; reads return 403 and it stays hidden from discovery. The server still starts and the error is logged so operators can spot the broken entry.
  • Token-gated namespaces (non-empty token array) are only visible in GET /namespaces to callers who provide a valid token (or admin token).
  • Each object in the array represents a token with the following fields:
FieldTypeRequiredDescription
tokenstringYesAuthentication token string
publishbooleanNoAllow publishing (default false)
markstringNoToken purpose description
expiresAtstringNoISO 8601 expiration time; returns 401 once passed

Generate tokens with openssl rand -hex 32 (≥ 32 characters of randomness) and split namespaces into separate read-only and publish tokens.

Complete Configuration Example

json
{
  "@rack": [],
  "@public": [],

  "@company": [
    {
      "token": "a3f9c8e7b2d1f4e6a9c7b5d8f3e1a2c4",
      "mark": "Team read-only access"
    },
    {
      "token": "b6d9e7f1a3c5b8d2e4f7a9c1b3d5e8f2",
      "publish": true,
      "mark": "CI/CD publishing service",
      "expiresAt": "2025-12-31T23:59:59Z"
    }
  ],

  "@private": [
    {
      "token": "c9e2f5a8b1d4c7e3f6a9b2c5d8e1f4a7",
      "publish": true,
      "mark": "Internal publishing system"
    }
  ]
}

Webhook Configuration

The webhook configuration file is located at apps/registry-server/config/webhooks.json, used to configure event notifications.

Configuration File Structure

json
{
  "webhooks": [
    {
      "url": "https://example.com/webhook",
      "secret": "webhook-secret-key",
      "events": ["uploaded"],
      "enabled": true,
      "description": "description"
    }
  ]
}

Field Description

FieldTypeRequiredDescription
urlstringYesWebhook endpoint URL
secretstringYesHMAC-SHA256 signing key
eventsstring[]YesSubscribed event types
enabledbooleanYesWhether enabled
descriptionstringNoWebhook description

Supported Event Types

  • uploaded - Triggered after Registry package upload succeeds
  • version.created - Triggered after new version is installed and versions.json is updated

Event Trigger Order

Both uploaded and version.created events are emitted after the full upload pipeline completes (install + versions.json update). They are fired in sequence at the end of the process.

Configuration Examples

1. Single Webhook

json
{
  "webhooks": [
    {
      "url": "https://ci.company.com/webhook",
      "secret": "webhook-secret-2024",
      "events": ["uploaded"],
      "enabled": true,
      "description": "Trigger CI/CD pipeline"
    }
  ]
}

2. Multiple Webhooks

json
{
  "webhooks": [
    {
      "url": "https://ci.company.com/webhook",
      "secret": "ci-webhook-secret",
      "events": ["uploaded"],
      "enabled": true,
      "description": "CI/CD automatic build"
    },
    {
      "url": "https://notify.company.com/slack",
      "secret": "slack-webhook-secret",
      "events": ["uploaded", "version.created"],
      "enabled": true,
      "description": "Slack notification (subscribe to multiple events)"
    },
    {
      "url": "https://staging.company.com/webhook",
      "secret": "staging-secret",
      "events": ["uploaded"],
      "enabled": false,
      "description": "Staging environment (disabled)"
    }
  ]
}

Webhook Event Format

When an event is triggered, Registry Server sends a POST request to the configured URL:

Request Headers

Content-Type: application/json
User-Agent: Rack-Registry-Webhook/1.0
X-Webhook-Event: uploaded
X-Webhook-Signature: sha256=...
X-Webhook-Timestamp: 2025-11-07T10:30:00.000Z
X-Webhook-Delivery: unique-id

Request Body

json
{
  "event": "uploaded",
  "timestamp": "2025-11-07T10:30:00.000Z",
  "namespace": "@company",
  "name": "ui-kit",
  "version": "1.0.0",
  "path": "@company/ui-kit/1.0.0"
}

Verifying Webhook Signatures

Verify signatures on the webhook receiver side (Node.js example):

javascript
const crypto = require('crypto')

function verifyWebhookSignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret)
  hmac.update(payload)
  const expected = `sha256=${hmac.digest('hex')}`

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

// Using in Express
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-webhook-signature']
  const payload = JSON.stringify(req.body)

  if (!verifyWebhookSignature(payload, signature, 'your-secret')) {
    return res.status(401).send('Invalid signature')
  }

  // Handle webhook event
  console.log('Event received:', req.body)
  res.status(200).send('OK')
})

Webhook Retry

  • Failed webhooks will automatically retry up to 3 times (4 total attempts)
  • Retry intervals: 2 seconds, 4 seconds, 8 seconds (exponential backoff)
  • Each delivery attempt has a 30-second timeout; no response within 30 seconds is treated as a failure
  • A 2xx status code is considered successful
  • The webhook queue is in-memory only; pending retries are lost if the process restarts. Implement idempotent handling on the receiver side if guaranteed delivery is required

Released under the MIT License.