Files
openwork/services/den-v2/README.md
ben 0e88389849 feat(den): add daytona-backed docker dev flow (#918)
* feat(den): add daytona-backed docker dev flow

* fix(den): allow multiple cloud workers in dev

* fix(den): use Daytona snapshots for sandbox runtime

Use a prebuilt Daytona snapshot for the dev worker runtime so sandboxes start with openwork and opencode already installed. Pass the snapshot through the local Docker flow and add a helper to build the snapshot image for repeatable setup.

* chore(den): lower Daytona snapshot defaults

Reduce the default snapshot footprint to 1 CPU and 2GB RAM so local Daytona worker testing fits smaller org limits more easily.

* Omar is comfortable

Make Daytona-backed cloud workers stable enough to reconnect through a dedicated proxy instead of persisting expiring signed preview URLs. Split the proxy into its own deployable service, share Den schema access through a common package, and fix the web badge so healthy workers show ready.

* chore(den-db): add Drizzle package scripts

Move the shared schema package toward owning its own migration workflow by adding generate and migrate commands plus a local Drizzle config.

* chore: update lockfile

Refresh the workspace lockfile so the new den-db Drizzle tooling is captured in pnpm-lock.yaml.

* feat(den-worker-proxy): make Vercel deployment-ready

Align the proxy service with Vercel's Hono runtime entry pattern and keep a separate Node server entry for Docker/local runs. Also scaffold the Vercel project/env setup and wire Render deploy sync to pass Daytona variables needed for daytona mode.

* feat(den-db): add db mode switch for PlanetScale

Support DB_MODE=planetscale with Drizzle's PlanetScale serverless driver while keeping mysql2 as the local default. This lets Vercel-hosted services use HTTP database access without changing local development workflows.

* refactor(den-db): adopt shared TypeID ids

Move the Den TypeID system into a shared utils package and use it across auth, org, worker, and sandbox records so fresh databases get one consistent internal ID format. Wire Better Auth into the same generator and update Den request boundaries to normalize typed ids cleanly.

* fix(den): restore docker dev stack after refactor

Include the shared utils package in the Den Docker images, expose MySQL to the host for local inspection, and fix the remaining Den build/runtime issues surfaced by the Docker path after the shared package and TypeID changes.

* docs(den): document Daytona snapshot setup

Add README guidance for building and publishing the prebuilt Daytona runtime snapshot, including the helper script, required env, and how to point Den at the snapshot for local Daytona mode.

* refactor(den-db): reset migrations and load env files

Replace the old Den SQL migration history with a fresh baseline for the current schema, and let Drizzle commands load database credentials from env files. Default to mysql when DATABASE_URL is present and otherwise use PlanetScale credentials so local Docker and hosted environments can share the same DB package cleanly.

* fix(den): prepare manual PlanetScale deploys

Update the Render workflow and Docker build path for the shared workspace packages, support PlanetScale credentials in the manual SQL migration runner, and stop auto-running DB migrations on Den startup so schema changes stay manual.

* feat(den-v2): add Daytona-first control plane

Create a new den-v2 service from the current Daytona-enabled control plane, default it to Daytona provisioning, and add a dedicated Render deployment workflow targeting the new v2 Render service.

* feat(den-worker-proxy): redirect root to landing

Send root proxy traffic to openworklabs.com so direct visits to the worker proxy domain do not hit worker-resolution errors.

---------

Co-authored-by: OmarMcAdam <gh@mcadam.io>
2026-03-16 21:20:26 -07:00

219 lines
9.6 KiB
Markdown

# Den v2 Service
Control plane for hosted workers. Provides Better Auth, worker CRUD, and provisioning hooks.
## Quick start
```bash
pnpm install
cp .env.example .env
pnpm dev
```
## Docker dev stack
For a one-command local stack with MySQL + the Den cloud web app, run this from the repo root:
```bash
./packaging/docker/den-dev-up.sh
```
That brings up:
- local MySQL for Den
- the Den control plane on a randomized host port
- the OpenWork Cloud web app on a randomized host port
The script prints the exact URLs and `docker compose ... down` command to use for cleanup.
## Environment
- `DATABASE_URL` MySQL connection URL
- `BETTER_AUTH_SECRET` 32+ char secret
- `BETTER_AUTH_URL` public base URL Better Auth uses for OAuth redirects and callbacks
- `DEN_BETTER_AUTH_TRUSTED_ORIGINS` optional comma-separated trusted origins for Better Auth origin validation (defaults to `CORS_ORIGINS`)
- `GITHUB_CLIENT_ID` optional OAuth app client ID for GitHub sign-in
- `GITHUB_CLIENT_SECRET` optional OAuth app client secret for GitHub sign-in
- `GOOGLE_CLIENT_ID` optional OAuth app client ID for Google sign-in
- `GOOGLE_CLIENT_SECRET` optional OAuth app client secret for Google sign-in
- `PORT` server port
<<<<<<< HEAD
- `CORS_ORIGINS` comma-separated list of trusted browser origins (used for Better Auth origin validation + Express CORS)
- `PROVISIONER_MODE` `stub`, `render`, or `daytona`
- `OPENWORK_DAYTONA_ENV_PATH` optional path to a shared `.env.daytona` file; when unset, Den searches upwards from the repo for `.env.daytona`
- `WORKER_URL_TEMPLATE` template string with `{workerId}`
- `RENDER_API_BASE` Render API base URL (default `https://api.render.com/v1`)
- `RENDER_API_KEY` Render API key (required for `PROVISIONER_MODE=render`)
- `RENDER_OWNER_ID` Render workspace owner id (required for `PROVISIONER_MODE=render`)
- `RENDER_WORKER_REPO` repository URL used to create worker services
- `RENDER_WORKER_BRANCH` branch used for worker services
- `RENDER_WORKER_ROOT_DIR` render `rootDir` for worker services
- `RENDER_WORKER_PLAN` Render plan for worker services
- `RENDER_WORKER_REGION` Render region for worker services
- `RENDER_WORKER_OPENWORK_VERSION` `openwork-orchestrator` npm version installed in workers; the worker build uses its `opencodeVersion` metadata to bundle a matching `opencode` binary into the Render deploy
- `RENDER_WORKER_NAME_PREFIX` service name prefix
- `RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX` optional domain suffix for worker custom URLs (e.g. `openwork.studio` -> `<worker-id>.openwork.studio`)
- `RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS` max time to wait for vanity URL health before falling back to Render URL
- `RENDER_PROVISION_TIMEOUT_MS` max time to wait for deploy to become live
- `RENDER_HEALTHCHECK_TIMEOUT_MS` max time to wait for worker health checks
- `RENDER_POLL_INTERVAL_MS` polling interval for deploy + health checks
- `VERCEL_API_BASE` Vercel API base URL (default `https://api.vercel.com`)
- `VERCEL_TOKEN` Vercel API token used to upsert worker DNS records
- `VERCEL_TEAM_ID` optional Vercel team id for scoped API calls
- `VERCEL_TEAM_SLUG` optional Vercel team slug for scoped API calls (used when `VERCEL_TEAM_ID` is unset)
- `VERCEL_DNS_DOMAIN` Vercel-managed DNS zone used for worker records (default `openwork.studio`)
- `POLAR_FEATURE_GATE_ENABLED` enable cloud-worker paywall (`true` or `false`)
- `POLAR_API_BASE` Polar API base URL (default `https://api.polar.sh`)
- `POLAR_ACCESS_TOKEN` Polar organization access token (required when paywall enabled)
- `POLAR_PRODUCT_ID` Polar product ID used for checkout sessions (required when paywall enabled)
- `POLAR_BENEFIT_ID` Polar benefit ID required to unlock cloud workers (required when paywall enabled)
- `POLAR_SUCCESS_URL` redirect URL after successful checkout (required when paywall enabled)
- `POLAR_RETURN_URL` return URL shown in checkout (required when paywall enabled)
- Daytona:
- `DAYTONA_API_KEY` API key used to create sandboxes and volumes
- `DAYTONA_API_URL` Daytona API base URL (default `https://app.daytona.io/api`)
- `DAYTONA_TARGET` optional Daytona region/target
- `DAYTONA_SNAPSHOT` optional snapshot name; if omitted Den creates workers from `DAYTONA_SANDBOX_IMAGE`
- `DAYTONA_SANDBOX_IMAGE` sandbox base image when no snapshot is provided (default `node:20-bookworm`)
- `DAYTONA_SANDBOX_CPU`, `DAYTONA_SANDBOX_MEMORY`, `DAYTONA_SANDBOX_DISK` resource sizing when image-backed sandboxes are used
- `DAYTONA_SANDBOX_AUTO_STOP_INTERVAL`, `DAYTONA_SANDBOX_AUTO_ARCHIVE_INTERVAL`, `DAYTONA_SANDBOX_AUTO_DELETE_INTERVAL` lifecycle controls
- `DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS` TTL for the signed OpenWork preview URL returned to Den clients (Daytona currently caps this at 24 hours)
- `DAYTONA_SANDBOX_NAME_PREFIX`, `DAYTONA_VOLUME_NAME_PREFIX` resource naming prefixes
- `DAYTONA_WORKSPACE_MOUNT_PATH`, `DAYTONA_DATA_MOUNT_PATH` volume mount paths inside the sandbox
- `DAYTONA_RUNTIME_WORKSPACE_PATH`, `DAYTONA_RUNTIME_DATA_PATH`, `DAYTONA_SIDECAR_DIR` local sandbox paths used for the live OpenWork runtime; the mounted Daytona volumes are linked into the runtime workspace under `volumes/`
- `DAYTONA_OPENWORK_PORT`, `DAYTONA_OPENCODE_PORT` ports used when launching `openwork serve`
- `DAYTONA_OPENWORK_VERSION` optional npm version to install instead of latest `openwork-orchestrator`
- `DAYTONA_CREATE_TIMEOUT_SECONDS`, `DAYTONA_DELETE_TIMEOUT_SECONDS`, `DAYTONA_HEALTHCHECK_TIMEOUT_MS`, `DAYTONA_POLL_INTERVAL_MS` provisioning timeouts
For local Daytona development, place your Daytona API credentials in `/_repos/openwork/.env.daytona` and Den will pick them up automatically, including from task worktrees.
## Building a Daytona snapshot
If you want Daytona workers to start from a prebuilt runtime instead of a generic base image, create a snapshot and point Den at it.
The snapshot builder for this repo lives at:
- `scripts/create-daytona-openwork-snapshot.sh`
- `services/den-worker-runtime/Dockerfile.daytona-snapshot`
It builds a Linux image with:
- `openwork-orchestrator`
- `opencode`
Prerequisites:
- Docker running locally
- Daytona CLI installed and logged in
- a valid `.env.daytona` with at least `DAYTONA_API_KEY`
From the OpenWork repo root:
```bash
./scripts/create-daytona-openwork-snapshot.sh
```
To publish a custom-named snapshot:
```bash
./scripts/create-daytona-openwork-snapshot.sh openwork-runtime
```
Useful optional overrides:
- `DAYTONA_SNAPSHOT_NAME`
- `DAYTONA_SNAPSHOT_REGION`
- `DAYTONA_SNAPSHOT_CPU`
- `DAYTONA_SNAPSHOT_MEMORY`
- `DAYTONA_SNAPSHOT_DISK`
- `OPENWORK_ORCHESTRATOR_VERSION`
- `OPENCODE_VERSION`
After the snapshot is pushed, set it in `.env.daytona`:
```env
DAYTONA_SNAPSHOT=openwork-runtime
```
Then start Den in Daytona mode:
```bash
DEN_PROVISIONER_MODE=daytona packaging/docker/den-dev-up.sh
```
If you do not set `DAYTONA_SNAPSHOT`, Den falls back to `DAYTONA_SANDBOX_IMAGE` and installs runtime dependencies at sandbox startup.
## Auth setup (Better Auth)
Generate Better Auth schema (Drizzle):
```bash
npx @better-auth/cli@latest generate --config src/auth.ts --output src/db/better-auth.schema.ts --yes
```
Apply migrations:
```bash
pnpm db:generate
pnpm db:migrate
# or use the SQL migration runner used by Docker
pnpm db:migrate:sql
```
## API
- `GET /health`
- `GET /` demo web app (sign-up + auth + worker launch)
- `GET /v1/me`
- `GET /v1/workers` (list recent workers for signed-in user/org)
- `POST /v1/workers`
- Cloud launches return `202` quickly with worker `status=provisioning` and continue provisioning asynchronously.
- Returns `402 payment_required` with Polar checkout URL when paywall is enabled and entitlement is missing.
- Existing Polar customers are matched by `external_customer_id` first, then by email to preserve access for pre-existing paid users.
- `GET /v1/workers/:id`
- Includes latest instance metadata when available.
- `POST /v1/workers/:id/tokens`
- `DELETE /v1/workers/:id`
- Deletes worker records and attempts to tear down the backing cloud runtime when destination is `cloud`.
## CI deployment (dev == prod)
The workflow `.github/workflows/deploy-den.yml` updates Render env vars and deploys the service on every push to `dev` when this service changes.
Required GitHub Actions secrets:
- `RENDER_API_KEY`
- `RENDER_DEN_CONTROL_PLANE_SERVICE_ID`
- `RENDER_OWNER_ID`
- `DEN_DATABASE_URL`
- `DEN_BETTER_AUTH_SECRET`
Optional GitHub Actions secrets (enable GitHub social sign-in):
- `DEN_GITHUB_CLIENT_ID`
- `DEN_GITHUB_CLIENT_SECRET`
- `DEN_GOOGLE_CLIENT_ID`
- `DEN_GOOGLE_CLIENT_SECRET`
Optional GitHub Actions variable:
- `DEN_RENDER_WORKER_PLAN` (defaults to `standard`)
- `DEN_RENDER_WORKER_OPENWORK_VERSION` pins the `openwork-orchestrator` npm version installed in workers; the worker build bundles the matching `opencode` release asset into the Render image
- `DEN_CORS_ORIGINS` (defaults to `https://app.openwork.software,https://api.openwork.software,<render-service-url>`)
- `DEN_BETTER_AUTH_TRUSTED_ORIGINS` (defaults to `DEN_CORS_ORIGINS`)
- `DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX` (defaults to `openwork.studio`)
- `DEN_RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS` (defaults to `240000`)
- `DEN_BETTER_AUTH_URL` (defaults to `https://app.openwork.software`)
- `DEN_VERCEL_API_BASE` (defaults to `https://api.vercel.com`)
- `DEN_VERCEL_TEAM_ID` (optional)
- `DEN_VERCEL_TEAM_SLUG` (optional, defaults to `prologe`)
- `DEN_VERCEL_DNS_DOMAIN` (defaults to `openwork.studio`)
- `DEN_POLAR_FEATURE_GATE_ENABLED` (`true`/`false`, defaults to `false`)
- `DEN_POLAR_API_BASE` (defaults to `https://api.polar.sh`)
- `DEN_POLAR_SUCCESS_URL` (defaults to `https://app.openwork.software`)
- `DEN_POLAR_RETURN_URL` (defaults to `DEN_POLAR_SUCCESS_URL`)
Required additional secret when using vanity worker domains:
- `VERCEL_TOKEN`