Skip to content

Commit a6ed642

Browse files
committed
Update files
1 parent 64bf730 commit a6ed642

17 files changed

Lines changed: 540 additions & 9 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added — Cloudflare Containers deployment for `apps/objectos` & `apps/cloud`
11+
- **`apps/{objectos,cloud}/scripts/deploy-cloudflare.sh`** — Idempotent `build → push → deploy` pipeline. Reads config from `.env.cloudflare` (gitignored) or env vars; auto-tags images with the current git short SHA; in-place rewrites the `image = "..."` line in `wrangler.toml` (BSD/GNU sed compatible); supports `--tag`, `--skip-build`, `--skip-push`, `--skip-deploy`, `--dry-run`. Forces `--platform linux/amd64` (Cloudflare Containers requirement).
12+
- **`apps/{objectos,cloud}/scripts/setup-cloudflare-secrets.sh`** — Bulk `wrangler secret put` from a local `.env.cloudflare.secrets` file. Per-app key allow-list lets one shared file feed both Workers; unset keys are skipped (not cleared). Safe to re-run.
13+
- **npm scripts** in both apps: `cf:build`, `cf:push`, `cf:deploy`, `cf:deploy:dry`, `cf:secrets`, `cf:tail`.
14+
- **`.env.cloudflare.example` + `.env.cloudflare.secrets.example`** templates per app, with all required keys documented and the real files added to each app's `.gitignore`.
15+
1116
- **`apps/cloud/Dockerfile` + `.dockerignore`** — Production multi-stage image mirroring `apps/objectos/Dockerfile` (Node 22, pnpm workspace builder, slim runtime). Defaults `PORT=4000` to match `pnpm dev`, sets `OS_DISABLE_CONSOLE=1`, builds `@objectstack/cloud...` and serves via `objectstack serve --prebuilt`.
1217
- **`apps/{objectos,cloud}/wrangler.toml`** — Cloudflare Containers configs that wrap the production image in a Container-class Durable Object (`ObjectOSContainer` / `CloudContainer`) and front it with a fetch-forwarding Worker. Uses `instance_type = "standard-1"`, `nodejs_compat`, `[[migrations]] new_sqlite_classes`, and references a pre-pushed image tag (recommended workflow: `docker build` from repo root → `wrangler containers push``wrangler deploy`).
1318
- **`apps/{objectos,cloud}/cloudflare/worker.ts`** — Tiny Worker entrypoint using `@cloudflare/containers`. Pinned single-instance routing (`getContainer(env.X, 'singleton')`), inline `envVars` block for non-secret runtime config; secrets (`OS_DATABASE_URL`, `OS_DATABASE_AUTH_TOKEN`, `AUTH_SECRET`, `TURSO_*`) are injected via `wrangler secret put`.

apps/cloud/.env.cloudflare.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Cloudflare Containers deployment config — non-secret defaults.
2+
#
3+
# Copy to `.env.cloudflare` (gitignored) and fill in your account id.
4+
# Secrets go in `.env.cloudflare.secrets` instead.
5+
6+
# Required: Cloudflare account id (run `npx wrangler whoami` to find it)
7+
CF_ACCOUNT_ID=2846eb40a60f4738e292b90dcd8cce10
8+
9+
# Optional overrides (defaults shown)
10+
# CF_IMAGE_REGISTRY=registry.cloudflare.com/$CF_ACCOUNT_ID
11+
# CF_IMAGE_NAME=objectstack-cloud
12+
# CF_IMAGE_TAG=$(git rev-parse --short HEAD)
13+
# CF_PLATFORM=linux/amd64
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Cloudflare Worker secrets — copy to `.env.cloudflare.secrets` (gitignored)
2+
# and fill in. `cf:secrets` will skip any unset key, so it is safe to share
3+
# this file across both apps.
4+
5+
# ── Required for both apps ─────────────────────────────────────────────────
6+
# Remote libSQL/Turso URL — Cloudflare Containers' filesystem is ephemeral,
7+
# do NOT use file:/data/...
8+
OS_DATABASE_URL=
9+
OS_DATABASE_AUTH_TOKEN=
10+
# Cookie/session signing secret. Generate with: openssl rand -hex 32
11+
AUTH_SECRET=
12+
13+
# ── Cloud-only (apps/cloud) ────────────────────────────────────────────────
14+
# Used by the project provisioning workflow to create per-project Turso DBs.
15+
TURSO_API_TOKEN=
16+
TURSO_ORG_NAME=
17+
18+
# ── ObjectOS multi-project mode (apps/objectos) ────────────────────────────
19+
# Point at your apps/cloud Worker for multi-project mode. Leave unset for
20+
# single-project local mode (you'll then need OS_DATABASE_URL above).
21+
OS_CLOUD_URL=
22+
OS_CLOUD_API_KEY=
23+
24+
# ── Optional (both apps) ───────────────────────────────────────────────────
25+
# Set when binding a custom domain so cookies span subdomains.
26+
# OS_COOKIE_DOMAIN=.your-domain.com

apps/cloud/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ node_modules/
2121
Thumbs.db
2222
.vercel
2323
.env*.local
24+
25+
# Cloudflare Containers deploy config (real values, not examples)
26+
.env.cloudflare
27+
.env.cloudflare.secrets

apps/cloud/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ RUN corepack enable
3232

3333
# Bring in lockfile + workspace manifest first to maximise layer cache hits
3434
# on subsequent rebuilds where only source code changes.
35-
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsup.config.ts ./
35+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.json tsup.config.ts ./
3636

3737
# Copy every workspace member required at runtime.
3838
# Studio / docs are excluded to keep the image slim (see .dockerignore).
@@ -44,7 +44,7 @@ RUN pnpm install --frozen-lockfile --prod=false
4444

4545
# Build upstream packages that apps/cloud depends on via workspace: protocol.
4646
# `...` expands to "package + all transitive deps in the workspace".
47-
RUN pnpm --filter '@objectstack/cloud...' build || true
47+
RUN pnpm --filter '@objectstack/cloud...' build
4848

4949

5050
# ──────────────────────────────────────────────────────────────────────────

apps/cloud/README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,38 @@ Files:
5656
- `wrangler.toml` — Worker + Container binding.
5757
- `cloudflare/worker.ts` — fetch handler that proxies HTTP into the
5858
`CloudContainer` Durable Object.
59+
- `scripts/deploy-cloudflare.sh``build → push → deploy` pipeline.
60+
- `scripts/setup-cloudflare-secrets.sh` — bulk `wrangler secret put` from
61+
a local env file.
5962

60-
Quick start (see `wrangler.toml` for the full workflow):
63+
### Quickstart (automated)
64+
65+
```bash
66+
# One-time setup
67+
npx wrangler login
68+
cp apps/cloud/.env.cloudflare.example apps/cloud/.env.cloudflare
69+
cp apps/cloud/.env.cloudflare.secrets.example apps/cloud/.env.cloudflare.secrets
70+
# Fill in CF_ACCOUNT_ID + secrets (see comments inside each file)
71+
72+
# Push secrets (once, or any time they change)
73+
pnpm --filter @objectstack/cloud cf:secrets
74+
75+
# Build → push → deploy
76+
pnpm --filter @objectstack/cloud cf:deploy
77+
78+
# Live tail
79+
pnpm --filter @objectstack/cloud cf:tail
80+
```
81+
82+
Useful flags on `cf:deploy`: `--tag <name>`, `--skip-build`, `--skip-push`,
83+
`--dry-run`.
84+
85+
### Manual (if you don't want the script)
6186

6287
```bash
6388
# Build from repo root (Dockerfile expects the full pnpm workspace)
64-
docker build -f apps/cloud/Dockerfile \
89+
docker buildx build --platform linux/amd64 \
90+
-f apps/cloud/Dockerfile \
6591
-t registry.cloudflare.com/<account-id>/objectstack-cloud:latest .
6692

6793
wrangler containers push \

apps/cloud/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
"typecheck": "tsc --noEmit",
1313
"test": "objectstack test",
1414
"test:production-flow": "tsx test/production-flow.test.ts",
15+
"cf:build": "bash scripts/deploy-cloudflare.sh --skip-push --skip-deploy",
16+
"cf:push": "bash scripts/deploy-cloudflare.sh --skip-build --skip-deploy",
17+
"cf:deploy": "bash scripts/deploy-cloudflare.sh",
18+
"cf:deploy:dry": "bash scripts/deploy-cloudflare.sh --dry-run",
19+
"cf:secrets": "bash scripts/setup-cloudflare-secrets.sh",
20+
"cf:tail": "wrangler tail --config wrangler.toml",
1521
"clean": "rm -rf dist node_modules"
1622
},
1723
"dependencies": {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
# ─────────────────────────────────────────────────────────────────────────────
3+
# deploy-cloudflare.sh — build → push → deploy ObjectStack Cloud to
4+
# Cloudflare Containers.
5+
#
6+
# Mirror of apps/objectos/scripts/deploy-cloudflare.sh — see that file's
7+
# header for full usage. Only the app name / default image differ.
8+
# ─────────────────────────────────────────────────────────────────────────────
9+
set -euo pipefail
10+
11+
APP_NAME="objectstack-cloud"
12+
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13+
REPO_ROOT="$(cd "$APP_DIR/../.." && pwd)"
14+
WRANGLER_TOML="$APP_DIR/wrangler.toml"
15+
DOCKERFILE="$APP_DIR/Dockerfile"
16+
ENV_FILE="$APP_DIR/.env.cloudflare"
17+
18+
if [[ -f "$ENV_FILE" ]]; then
19+
echo "→ loading $ENV_FILE"
20+
set -a; source "$ENV_FILE"; set +a
21+
fi
22+
23+
: "${CF_IMAGE_NAME:=$APP_NAME}"
24+
: "${CF_IMAGE_TAG:=$(cd "$REPO_ROOT" && git rev-parse --short HEAD 2>/dev/null || echo latest)}"
25+
: "${CF_PLATFORM:=linux/amd64}"
26+
27+
SKIP_BUILD=0; SKIP_PUSH=0; SKIP_DEPLOY=0; DRY_RUN=0
28+
while [[ $# -gt 0 ]]; do
29+
case "$1" in
30+
--skip-build) SKIP_BUILD=1; shift ;;
31+
--skip-push) SKIP_PUSH=1; shift ;;
32+
--skip-deploy) SKIP_DEPLOY=1; shift ;;
33+
--dry-run) DRY_RUN=1; shift ;;
34+
--tag) CF_IMAGE_TAG="$2"; shift 2 ;;
35+
--tag=*) CF_IMAGE_TAG="${1#--tag=}"; shift ;;
36+
-h|--help) grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'; exit 0 ;;
37+
*) echo "unknown arg: $1" >&2; exit 2 ;;
38+
esac
39+
done
40+
41+
if [[ -z "${CF_ACCOUNT_ID:-}" ]]; then
42+
echo "✗ CF_ACCOUNT_ID is required (set in $ENV_FILE or env)" >&2
43+
echo " Run: npx wrangler whoami" >&2
44+
exit 1
45+
fi
46+
: "${CF_IMAGE_REGISTRY:=registry.cloudflare.com/$CF_ACCOUNT_ID}"
47+
IMAGE="$CF_IMAGE_REGISTRY/$CF_IMAGE_NAME:$CF_IMAGE_TAG"
48+
49+
echo "════════════════════════════════════════════════════════════════"
50+
echo " App : $APP_NAME"
51+
echo " Repo : $REPO_ROOT"
52+
echo " Image : $IMAGE"
53+
echo " Platform: $CF_PLATFORM"
54+
echo " Wrangler: $WRANGLER_TOML"
55+
echo "════════════════════════════════════════════════════════════════"
56+
57+
run() { if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] $*"; else "$@"; fi; }
58+
59+
if [[ $SKIP_BUILD -eq 0 ]]; then
60+
echo ""
61+
echo "▶ [1/3] docker buildx build"
62+
command -v docker >/dev/null || { echo "✗ docker not installed" >&2; exit 1; }
63+
run docker buildx build \
64+
--platform "$CF_PLATFORM" \
65+
-f "$DOCKERFILE" \
66+
-t "$IMAGE" \
67+
--load \
68+
"$REPO_ROOT"
69+
else
70+
echo "▶ [1/3] skipped (--skip-build)"
71+
fi
72+
73+
if [[ $SKIP_PUSH -eq 0 ]]; then
74+
echo ""
75+
echo "▶ [2/3] wrangler containers push"
76+
run npx --yes wrangler containers push "$IMAGE"
77+
else
78+
echo "▶ [2/3] skipped (--skip-push)"
79+
fi
80+
81+
if [[ $SKIP_DEPLOY -eq 0 ]]; then
82+
echo ""
83+
echo "▶ [3/3] update wrangler.toml image → $IMAGE"
84+
if [[ $DRY_RUN -eq 0 ]]; then
85+
if [[ "$OSTYPE" == "darwin"* ]]; then
86+
sed -i '' -E "s|^image = \".*\"|image = \"$IMAGE\"|" "$WRANGLER_TOML"
87+
else
88+
sed -i -E "s|^image = \".*\"|image = \"$IMAGE\"|" "$WRANGLER_TOML"
89+
fi
90+
fi
91+
echo ""
92+
echo "▶ wrangler deploy"
93+
run npx --yes wrangler deploy --config "$WRANGLER_TOML"
94+
else
95+
echo "▶ [3/3] skipped (--skip-deploy)"
96+
fi
97+
98+
echo ""
99+
echo "✓ done — $IMAGE"
100+
echo " Tail logs : (cd $APP_DIR && npx wrangler tail)"
101+
echo " Health : curl https://$APP_NAME.<your-subdomain>.workers.dev/api/v1/health"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
2+
# ─────────────────────────────────────────────────────────────────────────────
3+
# setup-cloudflare-secrets.sh — bulk-push secrets to a Cloudflare Worker.
4+
#
5+
# Reads values from `.env.cloudflare.secrets` (gitignored) and pipes each
6+
# one to `wrangler secret put`. Safe to re-run — wrangler upserts secrets.
7+
#
8+
# pnpm --filter @objectstack/objectos cf:secrets
9+
# pnpm --filter @objectstack/cloud cf:secrets
10+
#
11+
# .env.cloudflare.secrets format (one key=value per line, # for comments):
12+
#
13+
# OS_DATABASE_URL=libsql://my-control.turso.io
14+
# OS_DATABASE_AUTH_TOKEN=eyJhbGciOi...
15+
# AUTH_SECRET=<openssl rand -hex 32>
16+
# # Cloud-only:
17+
# TURSO_API_TOKEN=...
18+
# TURSO_ORG_NAME=...
19+
# # ObjectOS multi-project mode:
20+
# OS_CLOUD_URL=https://objectstack-cloud.<sub>.workers.dev
21+
# OS_CLOUD_API_KEY=...
22+
#
23+
# Any unset variable is *skipped* (not cleared) so you can keep one shared
24+
# file across both apps.
25+
# ─────────────────────────────────────────────────────────────────────────────
26+
set -euo pipefail
27+
28+
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
29+
WRANGLER_TOML="$APP_DIR/wrangler.toml"
30+
SECRETS_FILE="${SECRETS_FILE:-$APP_DIR/.env.cloudflare.secrets}"
31+
32+
# Per-app key allow-list. Secrets not in the list are ignored so we can
33+
# share one .env.cloudflare.secrets file across apps without leaking
34+
# unrelated keys to a Worker that has no use for them.
35+
APP_BASENAME="$(basename "$APP_DIR")"
36+
case "$APP_BASENAME" in
37+
objectos)
38+
KEYS=( OS_DATABASE_URL OS_DATABASE_AUTH_TOKEN AUTH_SECRET
39+
OS_CLOUD_URL OS_CLOUD_API_KEY OS_COOKIE_DOMAIN ) ;;
40+
cloud)
41+
KEYS=( OS_DATABASE_URL OS_DATABASE_AUTH_TOKEN AUTH_SECRET
42+
TURSO_API_TOKEN TURSO_ORG_NAME OS_COOKIE_DOMAIN ) ;;
43+
*)
44+
echo "✗ unknown app dir: $APP_BASENAME" >&2; exit 1 ;;
45+
esac
46+
47+
if [[ ! -f "$SECRETS_FILE" ]]; then
48+
echo "✗ no $SECRETS_FILE — copy .env.cloudflare.secrets.example and fill it in" >&2
49+
exit 1
50+
fi
51+
52+
set -a; source "$SECRETS_FILE"; set +a
53+
54+
echo "→ pushing secrets to Worker defined in $WRANGLER_TOML"
55+
PUSHED=0; SKIPPED=0
56+
for key in "${KEYS[@]}"; do
57+
value="${!key:-}"
58+
if [[ -z "$value" ]]; then
59+
echo " · $key (skipped — not set)"
60+
SKIPPED=$((SKIPPED+1))
61+
continue
62+
fi
63+
echo "$key"
64+
printf '%s' "$value" | npx --yes wrangler secret put "$key" \
65+
--config "$WRANGLER_TOML" >/dev/null
66+
PUSHED=$((PUSHED+1))
67+
done
68+
69+
echo ""
70+
echo "✓ pushed=$PUSHED skipped=$SKIPPED"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Cloudflare Containers deployment config — non-secret defaults.
2+
#
3+
# Copy to `.env.cloudflare` (gitignored) and fill in your account id.
4+
# Secrets go in `.env.cloudflare.secrets` instead.
5+
6+
# Required: Cloudflare account id (run `npx wrangler whoami` to find it)
7+
CF_ACCOUNT_ID=
8+
9+
# Optional overrides (defaults shown)
10+
# CF_IMAGE_REGISTRY=registry.cloudflare.com/$CF_ACCOUNT_ID
11+
# CF_IMAGE_NAME=objectos
12+
# CF_IMAGE_TAG=$(git rev-parse --short HEAD)
13+
# CF_PLATFORM=linux/amd64

0 commit comments

Comments
 (0)