Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7325bc0
docs(spec): Workspace → ee/workspace merge design
Aug 11, 2026
027b43e
docs(plan): ee/workspace merge implementation plan
Aug 11, 2026
656b810
feat(ee): import breeze-workspace as ee/workspace (@breeze/ext-worksp…
Aug 11, 2026
00bc219
docs(ee): commercial license for the ee/ directory
Aug 11, 2026
d7dc58c
feat(extensions): built-in extension loading path; workspace is the f…
Aug 11, 2026
52ffc17
fix(extensions): classify workspace export columns; make the sweep an…
Aug 11, 2026
76785b8
feat(api): load built-in extensions at boot
Aug 11, 2026
cb2e9a2
build(docker): bake built-in ee/workspace migrations + web assets int…
Aug 11, 2026
cfa9f9a
build(docker): extend ee/workspace to the release image and dev hot-r…
Aug 11, 2026
8674105
build(api): make ee/workspace's third-party bundled deps explicit + g…
Aug 11, 2026
838b950
ci: run ee/workspace tests in breeze CI
Aug 11, 2026
a45e517
fix(api): narrow tsup.config.ts's defineConfig return type in the gua…
Aug 11, 2026
43ba0dc
fix(ci): guard the success-path kill in the ee/workspace boot step
Aug 11, 2026
3757b9e
chore(extensions): retire the workspace symlink dev flow; document th…
Aug 11, 2026
422b38d
fix: final-review fixes — CI boot-step process hygiene, cursor export…
Aug 11, 2026
8094cd5
fix(compose): pgvector-enabled postgres image — workspace built-in mi…
Aug 11, 2026
3c5112b
fix(ci): pgvector image in binary-source smoke workflows
Aug 11, 2026
0c18d5a
fix(extensions): resolve built-in root from bundle runtime contexts; …
Aug 11, 2026
e8a5604
fix(ci): build workspace web bundle in binary-source smoke workflows
Aug 11, 2026
1953418
feat(extensions): deployment enable flag for built-ins — workspace de…
Aug 12, 2026
49c6017
fix(extensions): publish only existing tables for disabled built-ins;…
Aug 12, 2026
588e8a4
fix(ci): boot-step shutdown watches the process group and ignores zom…
Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,21 @@ MANAGED_SOFTWARE_POLICY_MODE=compat
# BILLING_SERVICE_URL=
# BILLING_SERVICE_API_KEY=

# --------------------------------------------
# Built-in extensions (optional, OFF)
# --------------------------------------------
# First-party extensions ship inside the API image but are NOT loaded unless the
# deployment switches them on, so a deployment that does not want one pays
# nothing for it — no migrations, no schema, no infrastructure requirements.
# Each flag is strict: only the exact string "true" enables it.
#
# Workspace (file/email indexing, content search). Enabling it requires a
# PostgreSQL image with the pgvector extension available — its migrations run
# `CREATE EXTENSION vector` on API boot, and a stock postgres:16-alpine will
# fail the boot with `extension "vector" is not available`. The
# POSTGRES_IMAGE_REF default below already includes it.
# BREEZE_WORKSPACE_ENABLED=true

# --------------------------------------------
# Docker Deployment
# --------------------------------------------
Expand Down Expand Up @@ -851,7 +866,25 @@ BREEZE_BINARIES_IMAGE_REF=ghcr.io/lanternops/breeze/binaries:${BREEZE_VERSION}
# Breeze is tested against. Override to digest-pinned refs in production
# (e.g. `caddy@sha256:<digest>` from `docker buildx imagetools inspect caddy:2-alpine`).
CADDY_IMAGE_REF=caddy:2-alpine
POSTGRES_IMAGE_REF=postgres:16-alpine
# pgvector/pgvector is the official Postgres image + the vector extension
# pre-installed. Required only when BREEZE_WORKSPACE_ENABLED=true (above),
# whose migrations (ee/workspace/migrations/*.sql) run `CREATE EXTENSION
# vector` on API boot for content embeddings. Same Postgres major version as
# stock, just with the extension baked in — kept as the default so enabling
# Workspace later needs no database swap.
#
# UPGRADE CAVEAT — do NOT swap this image over an EXISTING data volume.
# postgres:16-alpine is built against musl; pgvector/pgvector:pg16 is built
# against glibc. The two disagree about collation versions, so starting one over
# a data directory initialized by the other logs a collation version mismatch
# and leaves every text index (and every unique constraint on a text column)
# potentially inconsistent until it is rebuilt — which can silently return wrong
# rows, not just fail loudly. If you are already running and want to switch,
# either dump and restore into a freshly-initialized volume, or run
# `REINDEX DATABASE <dbname>;` after the first start on the new image (and
# `ALTER DATABASE <dbname> REFRESH COLLATION VERSION;`). If you do not need
# Workspace, the simplest answer is to keep the image you already have.
POSTGRES_IMAGE_REF=pgvector/pgvector:pg16
REDIS_IMAGE_REF=redis:7-alpine
COTURN_IMAGE_REF=coturn/coturn:latest
#
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/ci-smoke-binary-source-github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,14 @@ jobs:
timeout-minutes: 10

services:
# pgvector-enabled image, though this job no longer NEEDS it: built-in
# extensions load only when their enable flag is set, and this job
# leaves BREEZE_WORKSPACE_ENABLED unset, so ee/workspace/migrations/
# *.sql (CREATE EXTENSION vector) never runs. Kept to match the image
# .env.example ships, so the boot proved here is the one self-hosters
# run — a stock postgres:16-alpine would boot identically.
postgres:
image: postgres:16-alpine
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: breeze
POSTGRES_PASSWORD: breeze
Expand Down Expand Up @@ -83,6 +89,11 @@ jobs:
- name: Build API
run: pnpm --filter=@breeze/api build

# NOTE: no `ee/workspace` web build here, deliberately — see the same
# note in ci-smoke-binary-source-local.yml. This job leaves
# BREEZE_WORKSPACE_ENABLED unset, so the built-in never loads and its
# dist/web is never required.

- name: Boot API in BINARY_SOURCE=github mode
env:
# Authenticate binarySync's github API call so we don't hit the
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/ci-smoke-binary-source-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ jobs:
timeout-minutes: 10

services:
# pgvector-enabled image, though this job no longer NEEDS it: built-in
# extensions load only when their enable flag is set, and this job
# leaves BREEZE_WORKSPACE_ENABLED unset, so ee/workspace/migrations/
# *.sql (CREATE EXTENSION vector) never runs. Kept to match the image
# .env.example ships, so the boot proved here is the one self-hosters
# run — a stock postgres:16-alpine would boot identically.
postgres:
image: postgres:16-alpine
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: breeze
POSTGRES_PASSWORD: breeze
Expand Down Expand Up @@ -73,6 +79,14 @@ jobs:
- name: Build API
run: pnpm --filter=@breeze/api build

# NOTE: no `ee/workspace` web build here, deliberately. Built-in
# extensions are OFF unless the deployment sets their enable flag, and
# this job leaves BREEZE_WORKSPACE_ENABLED unset — so it proves the
# DEFAULT-OFF boot from a plain checkout: no built-in migrations, no
# dist/web requirement (which is what used to force a build:web step
# here), and no pgvector dependency. Flag-ON coverage lives in the
# integration shard-1 boot step in ci.yml.

- name: Stage a fake local agent binary
env:
WORKSPACE: ${{ github.workspace }}
Expand Down
156 changes: 155 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ jobs:
- name: Type-check legacy extension adapter
run: pnpm --filter @breeze/extension-api typecheck

# ee/workspace's own unit + typecheck coverage (424 tests). The
# apps/api typecheck step below separately covers the server code
# through the static built-in-extension import; this covers the rest
# (web/, __tests__/) that apps/api's tsconfig doesn't reach.
- name: Test workspace (ee)
run: pnpm --filter @breeze/ext-workspace test

- name: Type-check workspace (ee)
run: pnpm --filter @breeze/ext-workspace typecheck

- name: Run API tests
run: pnpm test --filter=@breeze/api

Expand Down Expand Up @@ -1728,6 +1738,144 @@ jobs:
# within a shard still run sequentially (fileParallelism: false).
run: pnpm --filter=@breeze/api test:integration --shard=${{ matrix.shard }}/4

# ee/workspace's integration suite expects its own tables
# (workspace_sources, workspace_file_index, ...) to already exist on
# this DB — it has no self-provisioning of its own. Those tables are
# created by loadBuiltinExtensions() (apps/api/src/extensions/
# builtinExtensions.ts), which — unlike core migrations — runs ONLY at
# actual `apps/api` server boot (src/index.ts), never inside the
# vitest integration suite's globalSetup (which calls core
# autoMigrate() only; see src/db/autoMigrate.ts's planMigrations,
# which reads the LEGACY extensions/ dir, not the built-in registry).
# Boot the real server once, against this shard's already
# core-migrated :5433 DB, wait for its "loaded built-in" line, then
# stop it — the same `tsx src/index.ts` boot Task 4 of the ee-merge
# plan verified applies workspace's migrations (idempotently on
# repeat boots). Shard 1 only: a single-DB schema prerequisite for the
# step below, not a sharded suite in itself.
- name: Boot API once to apply built-in extension migrations (ee)
if: matrix.shard == 1
env:
DATABASE_URL: postgresql://breeze_test:breeze_test@localhost:5433/breeze_test
DATABASE_URL_APP: postgresql://breeze_app:breeze_test@localhost:5433/breeze_test
BREEZE_APP_DB_PASSWORD: breeze_test
POSTGRES_PASSWORD: breeze_test
REDIS_URL: redis://localhost:6380
JWT_SECRET: ci-smoke-k7x9Qm4pR2vL8nW5jT3yF6hB0cA1dE4gI7
APP_ENCRYPTION_KEY: ci-smoke-Xz9Lm3Kp7Wn2Qr5Tv8Yb0Hd4Fg6Jc1Ae3
MFA_ENCRYPTION_KEY: ci-smoke-Nm4Rp8Ws2Kv6Tq0Yh3Bd7Fg1Jc5Le9Ax2
AGENT_ENROLLMENT_SECRET: ci-smoke-agent-enrollment-secret-value
NODE_ENV: test
AUTO_MIGRATE: 'true'
API_PORT: '3199'
# Built-in extensions are OFF by default per deployment; this step
# exists only to apply workspace's migrations, so it must opt in.
# The shard DB is the pgvector-capable docker-compose.test.yml
# postgres, so `CREATE EXTENSION vector` succeeds here.
BREEZE_WORKSPACE_ENABLED: 'true'
run: |
# The fatal boot sentinel. A BARE `CRITICAL` is not usable as one:
# non-fatal worker-init messages also print `[CRITICAL]` and can race
# into this same log, which would fail the step on a boot that
# actually succeeded. Only `index.ts`'s startup-failure line means
# "boot died".
FATAL='\[CRITICAL\] API startup failed'
# Launch in its own process group (setsid) so the group can be
# killed as a unit below. `pnpm exec` execs a shell wrapper around
# `tsx`, which in turn is a node process running a child worker —
# SIGTERM sent to just $! (pnpm exec's PID) is not guaranteed to
# propagate to the actual tsx/node descendants, so a "killed" boot
# can leave a fully-booted API (schedulers, reconciler, ...)
# running into the next step against this same DB shard: a flake
# generator. `kill -- -"$PGID"` below sends to the whole group.
#
# $! is the setsid PID, which is NOT reliably the new group's ID
# (setsid may fork), so the group records its own ID from inside:
# the inner shell IS the group leader, and `$$` is therefore the
# pgid. Everything below kills by that recorded value.
setsid bash -c 'echo $$ > /tmp/ee-boot.pgid; exec pnpm --filter=@breeze/api exec tsx src/index.ts' > /tmp/ee-workspace-boot.log 2>&1 &
BOOT_PID=$!
if ! timeout 180 bash -c "until grep -qE 'loaded built-in \"workspace\"|$FATAL' /tmp/ee-workspace-boot.log 2>/dev/null; do sleep 1; done"; then
echo "Timed out waiting for the built-in extension boot line:"
cat /tmp/ee-workspace-boot.log
kill -- "-$(cat /tmp/ee-boot.pgid 2>/dev/null || echo "$BOOT_PID")" 2>/dev/null || true
exit 1
fi
if grep -q "$FATAL" /tmp/ee-workspace-boot.log; then
echo "API boot failed before loading built-in extensions:"
cat /tmp/ee-workspace-boot.log
kill -- "-$(cat /tmp/ee-boot.pgid 2>/dev/null || echo "$BOOT_PID")" 2>/dev/null || true
exit 1
fi
PGID="$(cat /tmp/ee-boot.pgid 2>/dev/null || echo "$BOOT_PID")"
# Guarded like the two failure branches above: under this step's
# default `bash -e`, an unguarded `kill` on an already-exited
# group (e.g. the process crashed on its own between logging the
# built-in load line and this kill) would abort the step on a bare
# "process not found" with no log content, making a correct
# migration load look like an unexplained CI failure. Dump the log
# so it's diagnosable either way — and then decide: an already-dead
# group that ALSO logged the fatal sentinel is a genuine boot
# failure that merely happened to log the built-in line first, and
# must fail the step rather than be waved through.
if ! kill -- "-$PGID" 2>/dev/null; then
echo "API process group had already exited after logging the built-in load line — dumping its output:"
cat /tmp/ee-workspace-boot.log
if grep -q "$FATAL" /tmp/ee-workspace-boot.log; then
echo "The API logged the built-in load line and then FAILED startup — the schema this step is"
echo "supposed to guarantee may be incomplete, so the workspace integration suite below would"
echo "fail confusingly. Failing here instead."
exit 1
fi
echo "::warning::API exited on its own after loading built-in extensions (no startup failure logged); migrations were applied, continuing."
fi
# Bound the wait for graceful shutdown instead of an unbounded
# `wait "$BOOT_PID"`, which could otherwise eat the whole job
# ceiling if the process ignores SIGTERM or hangs mid-shutdown.
# Poll the GROUP, not $BOOT_PID: the leader (pnpm's wrapper) can
# exit seconds before the node/tsx descendants finish shutting
# down, and watching only the leader cuts the grace period short
# (observed on the first live run of this step). Count only
# NON-ZOMBIE members: `kill -0` "succeeds" against a zombie, but a
# zombie is dead — unreaped only because its parent died with it —
# and cannot touch the DB. The step's shell cannot reap
# grandchildren; init does so asynchronously after SIGKILL, so a
# zombie-only group must be treated as gone, not as a survivor
# (also observed live: `[node]`/`[esbuild] <defunct>` tripping a
# naive `kill -0 -- -PGID` assert).
live_in_group() {
ps -eo pgid=,stat= | awk -v p="$PGID" '$1 == p && $2 !~ /^Z/' | wc -l
}
for _ in $(seq 1 30); do
[ "$(live_in_group)" -gt 0 ] || break
sleep 1
done
kill -9 -- "-$PGID" 2>/dev/null || true
wait "$BOOT_PID" 2>/dev/null || true
# Fail CLOSED on a genuinely surviving process group — but give
# SIGKILL a moment to land before judging (delivery is async).
for _ in $(seq 1 10); do
[ "$(live_in_group)" -gt 0 ] || break
sleep 1
done
if [ "$(live_in_group)" -gt 0 ]; then
echo "API process group $PGID survived SIGTERM and SIGKILL. A live API would keep writing to"
echo "this step's DB shard and poison the workspace integration suite that runs next."
ps -eo pid,pgid,stat,cmd | awk -v pgid="$PGID" '$2 == pgid' || true
exit 1
fi

- name: Integration-test workspace (ee)
if: matrix.shard == 1
env:
DATABASE_URL: postgresql://breeze_test:breeze_test@localhost:5433/breeze_test
DATABASE_URL_APP: postgresql://breeze_app:breeze_test@localhost:5433/breeze_test
# Set for the same reason as the boot step above: should any part of
# this suite boot the host or consult the built-in enable flag, it
# must see the extension it is testing as enabled.
BREEZE_WORKSPACE_ENABLED: 'true'
run: pnpm --filter @breeze/ext-workspace test:integration

# Dedicated exact-role enforcement runner. The general integration config
# excludes it because this suite manages a temporary BYPASSRLS role and
# must not inherit the shared TRUNCATE hooks. The preceding suite has
Expand Down Expand Up @@ -1832,7 +1980,13 @@ jobs:
BREEZE_PORTAL_IMAGE_REF=ghcr.io/lanternops/breeze/portal:latest
BREEZE_BINARIES_IMAGE_REF=ghcr.io/lanternops/breeze/binaries:latest
CADDY_IMAGE_REF=caddy:2-alpine
POSTGRES_IMAGE_REF=postgres:16-alpine
# Mirrors .env.example's default. This stack deliberately leaves
# BREEZE_WORKSPACE_ENABLED unset, so it exercises the DEFAULT-OFF
# built-in path end to end (no workspace migrations, no pgvector
# requirement); the image is pgvector-capable anyway so that a
# self-hoster who later flips the flag needs no database swap, which
# is exactly the shape .env.example ships.
POSTGRES_IMAGE_REF=pgvector/pgvector:pg16
REDIS_IMAGE_REF=redis:7-alpine
COTURN_IMAGE_REF=coturn/coturn:latest
# Required in production by config/validate.ts (#625) — without
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,9 +502,11 @@ Self-hosted: your data never leaves your infrastructure. Cloud-hosted: data is i

## License

Breeze is licensed under [AGPL-3.0](LICENSE).

You can use, modify, and self-host Breeze freely. If you modify Breeze and offer it as a service, you must open source your modifications under the same license.
Breeze is [AGPL-3.0](LICENSE) licensed, with one exception: everything under
the [`ee/`](ee/) directory is covered by the
[Breeze Commercial License](ee/LICENSE) (source-visible, requires a
commercial agreement for production use). This is the same open-core layout
used by projects like Cal.com.

---

Expand Down
15 changes: 15 additions & 0 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ COPY packages/shared/package.json ./packages/shared/
# creates the workspace symlinks the API's tsup bundle resolves at build time.
COPY packages/extension-api/package.json ./packages/extension-api/
COPY packages/extension-sdk/package.json ./packages/extension-sdk/
COPY packages/extension-web-sdk/package.json ./packages/extension-web-sdk/
COPY packages/extension-testkit/package.json ./packages/extension-testkit/
COPY ee/workspace/package.json ./ee/workspace/

# Install all dependencies
RUN pnpm install --frozen-lockfile --prefer-offline
Expand All @@ -36,12 +39,19 @@ ENV NODE_ENV=${NODE_ENV}
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
COPY --from=deps /app/packages ./packages
COPY --from=deps /app/ee ./ee

# Copy source code
COPY apps/api ./apps/api
COPY packages ./packages
COPY ee ./ee
COPY tsconfig.json ./

# Build the built-in workspace extension's web bundle. Its server code needs
# no separate build step here -- the API's own tsup build (below) bundles
# @breeze/ext-workspace source directly via noExternal: [/^@breeze\//].
RUN pnpm --filter @breeze/ext-workspace build:web

# Build the API
WORKDIR /app/apps/api
RUN pnpm build
Expand Down Expand Up @@ -73,6 +83,11 @@ COPY --from=deps --chown=hono:nodejs /app/apps/api/node_modules ./apps/api/node_
COPY --from=deps --chown=hono:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=hono:nodejs /app/packages ./packages

# Built-in extensions: this runner's WORKDIR ends at /app/apps/api (see below),
# and builtinRegistry.ts's CJS-bundle fallback resolves packageDir relative to
# process.cwd() -- so the copy destination must be apps/api/ee, not /app/ee.
COPY --from=builder --chown=hono:nodejs /app/ee ./apps/api/ee

# Copy migration files for auto-migrate on startup
COPY --from=builder --chown=hono:nodejs /app/apps/api/migrations ./apps/api/migrations
# Runtime-extension mount point: deployments deliver extensions as SIGNED
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@anthropic-ai/sdk": "^0.115.0",
"@aws-sdk/client-s3": "^3.1100.0",
"@aws-sdk/s3-request-presigner": "^3.1096.0",
"@breeze/ext-workspace": "workspace:*",
"@breeze/extension-api": "workspace:*",
"@breeze/extension-sdk": "workspace:*",
"@breeze/shared": "workspace:*",
Expand Down
Loading
Loading