Shukr Docs
Shukr Public Media Service

Setup & Run

Run the Shukr Public Media Service locally on Cloudflare Workers, with Neon (or local Postgres) and R2.

The media service is a Cloudflare Workers app (Hono). It stores metadata in Neon Postgres (over HTTP) and streams file bytes directly to Cloudflare R2 via presigned URLs. Local development runs the Worker with Wrangler.

Prerequisites

  • Node.js LTS (≥ 20) and pnpm 10 — the repo pins pnpm@10.33.0 (corepack enable picks it up).
  • Docker — optional, only for a fully local Postgres (otherwise use a Neon database).
  • A Cloudflare account with Workers + R2 enabled, and an R2 bucket (default shukr-public-media) with an S3 API token (Object Read & Write) for presigning.
  • ACCESS_TOKEN_SECRET shared with shukr-api (the same secret that signs user access tokens).

Wrangler is a dev dependency — no global install needed. All commands below run from the monorepo root unless noted; the root scripts proxy to the media-service package.

1. Install

git clone <repo-url> shukr-public-media-service
cd shukr-public-media-service
pnpm install

2. Environment variables

Generate the local env files from the example, then fill in real values:

pnpm setup:env

This creates two files in apps/media-service/ from .env.example:

FileRead byPurpose
.env.localDrizzle Kit (migrations)Your editable source of truth.
.dev.varsWrangler (pnpm dev)Generated copy the Worker loads.

Edit apps/media-service/.env.local, then re-run pnpm setup:env to sync .dev.vars.

Wrangler does not hot-reload .dev.vars — after changing env values, run pnpm setup:env again and restart pnpm dev. Never commit .env.local or .dev.vars.

Required values

# Cloudflare R2 (S3-compatible credentials for presigning)
R2_ACCOUNT_ID=your_cloudflare_account_id
R2_BUCKET_NAME=shukr-public-media
R2_ACCESS_KEY_ID=your_r2_access_key_id
R2_SECRET_ACCESS_KEY=your_r2_secret_access_key

# Postgres — postgresql:// (Neon HTTP in prod/shared, or local Docker Postgres)
DATABASE_URL=postgresql://user:password@ep-xxx.region.aws.neon.tech/neondb?sslmode=require

# Auth — must match shukr-api's access-token secret (min 32 chars)
ACCESS_TOKEN_SECRET=your_access_token_secret_min_32_chars

# Machine-to-machine key for backend callers (min 16 chars)
SERVICE_API_KEY=your_service_api_key_min_16_chars

URL-encode special characters in the DATABASE_URL password (@%40, !%21). Values not set fall back to the defaults in wrangler.toml / env.ts.

3. Database

Use a hosted Neon database (recommended — matches production's HTTP driver) or a local Postgres in Docker. Then apply the Drizzle migrations.

# Set DATABASE_URL to your Neon connection string in .env.local, then:
pnpm db:migrate

A localhost URL auto-selects the TCP pg driver for local dev; deployed Workers must use a Neon URL (HTTP driver) — Workers can't open raw TCP. Presigned uploads always target remote R2, so R2_BUCKET_NAME must match a real bucket even locally.

Other Drizzle commands (run from apps/media-service): pnpm db:generate (create a migration from schema changes) and pnpm db:push (push schema directly, for throwaway local DBs).

4. Run the Worker

pnpm dev

Wrangler serves the Worker at http://localhost:8787. Check it's alive:

curl http://localhost:8787/health

5. API docs (non-production)

With the Worker running and ENVIRONMENTproduction:

PathWhat
GET /docOpenAPI 3.1 JSON spec
GET /swaggerSwagger UI (basic auth SWAGGER_USERNAME / SWAGGER_PASSWORD)
GET /referenceScalar API reference (basic auth)

The API Reference in these docs is generated from the same /doc spec — regenerate it with pnpm --filter shukr-public-media-service openapi:export then pnpm --filter docs openapi:generate.

6. Browser uploads → R2 CORS

Because clients PUT/GET bytes directly to R2, the bucket needs CORS for your origin. Set it from the CLI (reads R2_BUCKET_NAME from .env.local, needs CLOUDFLARE_API_TOKEN):

cd apps/media-service
pnpm r2:cors:set     # applies docs/r2-cors-wrangler.json
pnpm r2:cors:list

Or paste a policy in the Cloudflare dashboard (R2 → bucket → Settings → CORS):

[
  {
    "AllowedOrigins": ["http://localhost:5199", "https://your-app.example.com"],
    "AllowedMethods": ["GET", "PUT", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

ExposeHeaders: ["ETag"] is required for multipart uploads — clients read each part's ETag from the PUT response. Start a new upload session after changing CORS.

Upload test UI

A local page that exercises single, multipart, and TUS uploads against your running Worker:

pnpm upload-test     # serves the harness at http://localhost:5199

7. Streaming providers (optional)

Only needed if you upload video (stream assets). Set DEFAULT_STREAM_PROVIDER and the provider's env vars:

  • Bunny — set BUNNY_STREAM_API_KEY, BUNNY_STREAM_LIBRARY_ID, BUNNY_STREAM_CDN_HOSTNAME, and BUNNY_STREAM_READ_ONLY_API_KEY (all four, or Bunny is treated as unconfigured). Point the library webhook at /api/v1/webhooks/stream/bunny.
  • Cloudflare Stream — set CLOUDFLARE_STREAM_API_TOKEN, then run pnpm stream:setup to register the webhook (and optional signing keys). Uses R2_ACCOUNT_ID as the account id.
  • On-prem — set ONPREM_ENCODER_API_URL (and optional ONPREM_WEBHOOK_SECRET); your encoder writes HLS/DASH outputs back to R2 and calls /api/v1/webhooks/stream/onprem.

See Webhooks for the callback contracts.

8. Quality checks

Run from apps/media-service (or with pnpm --filter shukr-public-media-service <script>):

pnpm test          # vitest (unit + route tests)
pnpm test:coverage # with coverage
pnpm typecheck     # tsc, app + tests
pnpm lint          # biome

9. Deploy

pnpm deploy        # wrangler deploy

Production reads config from wrangler.toml [vars] plus Wrangler secrets (never .dev.vars). Set the sensitive values once:

cd apps/media-service
wrangler secret put ACCESS_TOKEN_SECRET
wrangler secret put DATABASE_URL
wrangler secret put R2_ACCOUNT_ID
wrangler secret put R2_BUCKET_NAME
wrangler secret put R2_ACCESS_KEY_ID
wrangler secret put R2_SECRET_ACCESS_KEY
wrangler secret put SERVICE_API_KEY

Set ENVIRONMENT=production in production — it disables /doc, /swagger, and /reference.

On this page