# create-apollo-suite-monorepo

Scaffold a pnpm monorepo where your custom frontend lives alongside Apollo CMS
mounted as a **git submodule** backend (read-only — pull updates only).

## Usage

```bash
npx create-apollo-suite-monorepo my-site
```

With flags:

```bash
npx create-apollo-suite-monorepo my-site \
  --frontend-name "@my-site/frontend" \
  --db "postgresql://user:pass@localhost:5432/my-site" \
  --url "http://localhost:3000" \
  --locale th
```

## Result

```
my-site/
├── apps/
│   ├── frontend/          ← @my-site/frontend (Next.js skeleton)
│   └── backend/           ← git submodule → apollo-cms
├── package.json           ← root workspace
├── pnpm-workspace.yaml
├── .env.local             ← shared dev env
├── .gitmodules            ← submodule config
└── .gitignore
```

## Routing modes

### Single-origin (default)

Both apps share one public origin (the frontend). The frontend's
`next.config.ts` rewrites these paths to the backend so `/_next/*` doesn't
collide:

| Browser path                         | Goes to              |
| ------------------------------------ | -------------------- |
| `/`, your custom routes              | `apps/frontend`      |
| `/admin/*`                           | `apps/backend` (admin pages **and** their JS chunks) |
| `/api/auth/*`, `/api/v1/*`, `/api/admin/*`, `/api/email/*`, `/api/health`, `/api/mcp`, `/api/editing-presence/*` | `apps/backend` |
| `/uploads/*`                         | `apps/backend` (media) |

Apollo CMS reads `APOLLO_ASSET_PREFIX` (default `/admin`) and serves its built
JS under `<prefix>/_next/static/`. Because the prefix coincides with the admin
path, a single `/admin/:path*` rewrite covers both pages and chunks — no
separate asset rewrite is emitted. The frontend **must not** define routes at
`/admin`, `/api/auth`, `/api/v1`, etc.

You can override the prefix:

```bash
npx create-apollo-suite-monorepo my-app --admin-prefix /cms
```

When the prefix is anything other than `/admin`, the scaffold also emits a
matching `<prefix>/:path*` rewrite for backend chunks.

### Separate origins (fallback)

Pass `--admin-prefix none` (or `off`/`false`/`disabled`) to skip the rewrite
wiring. The backend runs at `http://localhost:3000` and the frontend at
`http://localhost:3001`. Useful when you'd rather deploy them on separate
subdomains (e.g. `cms.example.com` + `example.com`).

## Flags

| Flag                       | Default                                 | Description                          |
| -------------------------- | --------------------------------------- | ------------------------------------ |
| `--frontend-name <name>`   | `@<dir>/frontend`                       | Frontend `package.json` name         |
| `--backend-url <url>`      | `https://github.com/5Lab-Group-Co-Ltd/apollo-suite.git` | Submodule git URL          |
| `--backend-branch <name>`  | `main`                                  | Submodule branch to track            |
| `-d, --db <url>`           | _(prompted)_                            | `DATABASE_URL` for backend           |
| `-u, --url <url>`          | `:3001` single-origin / `:3000` separate | `NEXT_PUBLIC_SITE_URL`               |
| `-l, --locale <code>`      | `en`                                    | `NEXT_PUBLIC_DEFAULT_LOCALE`         |
| `--admin-prefix <path>`    | `/admin`                                | Single-origin admin/asset namespace; `none` to disable. Alias: `--asset-prefix` |
| `--apps <slug,slug>`       | _(asked)_                               | Suite apps to install besides the mandatory ones; `""` for mandatory only. See [Suite apps](#suite-apps) |
| `--skip-install`           | off                                     | Don't run `pnpm install`             |
| `--skip-submodule`         | off                                     | Don't add the git submodule          |
| `-h, --help`               | —                                       | Show help                            |

## After install

```bash
cd my-site
pnpm backend:setup   # push schema + seed apollo-cms
pnpm dev             # frontend :3001 + backend :3000 in parallel
```

In single-origin mode open `http://localhost:3001/admin` (the frontend port);
the rewrite proxies to the backend and Better Auth's cookies/origins line up
because `NEXT_PUBLIC_SITE_URL` and the backend's `trustedProxyHeaders` are wired
to the public origin.

## Suite apps

The scaffold writes `apollo.apps.json` at the project root: the suite apps the
project installs besides the mandatory ones. An app that is not listed gets no
tables, no screens and no jobs.

- **`--apps <slug,slug>`** — written as given, after checking it against the
  backend that was just cloned: every slug must be an app there, and every app
  it requires must be listed too. A missing required app is an error, never
  added for you.
- **No `--apps`, in a terminal** — the installer lists the apps in the cloned
  backend (read from its `apps/*/plugin.json`, never a list inside the
  installer) and asks.
- **No `--apps`, not a terminal, or `--skip-submodule`** — mandatory apps only.

`.env.local` gets `APOLLO_APPS_FILE=../../apollo.apps.json` (resolved against
`apps/backend`), and the root `package.json` gets an `apl` script:

```bash
pnpm apl apps                  # every app and its state
pnpm apl install <slug...>     # add apps — runs the upgrade that creates their tables
pnpm apl uninstall <slug>      # leave an app out — its tables and data are kept
pnpm apl doctor                # do the file, the apps and the database agree?
```

Before the database has been set up once, edit the list by hand and run
`pnpm backend:setup`; `apl install` needs the tables setup creates.

## Updating the backend

```bash
pnpm backend:update  # git submodule update --remote --merge apps/backend
                     # + node apps/backend/scripts/sync-root-workspace.mjs + pnpm install
```

Don't edit files inside `apps/backend` — open issues / PRs against the
`apollo-cms` repository upstream.

### Root `pnpm-workspace.yaml` mirrors the backend's overrides and patches

pnpm reads **only the root** `pnpm-workspace.yaml`; the submodule's own copy
is ignored once `apps/backend` is a package of the outer workspace. The
scaffold (and `pnpm backend:update`) therefore run
`node apps/backend/scripts/sync-root-workspace.mjs`, which copies the
backend's security `overrides` and `patchedDependencies` into the root file
(patch paths rewritten to `apps/backend/patches/…`). Your own root overrides
are kept; backend keys win on conflict.

Without the mirror the drizzle-kit patch never applies, and every
`drizzle-kit push` / `apl install` re-adds each `NULLS NOT DISTINCT` unique
constraint and prompts to truncate any non-empty table that owns one —
in CI (no TTY) the push aborts.

### Scaffolds created before `sync-root-workspace`

Run it once by hand, then reinstall:

```bash
node apps/backend/scripts/sync-root-workspace.mjs && pnpm install
```

and add `node apps/backend/scripts/sync-root-workspace.mjs` to your
`backend:update` script between the submodule update and `pnpm install`.

### Scaffolds created before v0.9.991

Two entries were missing from older scaffolds, and `pnpm dev` fails during
`plugins:build` without them:

```
error: Could not resolve: "ioredis"
error: Could not resolve: "@opentelemetry/api"
```

`apps/backend/plugins/*` has to be listed in the root `pnpm-workspace.yaml`
(apollo-cms's own workspace file is ignored once the submodule is a package of
an outer workspace, so the built-in plugins' dependencies never install), and
`@opentelemetry/api` has to be a root devDependency (bun's bundler resolves
`next`'s guarded `require()` of this optional peer, which pnpm — unlike bun —
does not install). Patch an existing scaffold with:

```yaml
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'apps/extensions/*'
  - 'apps/backend/plugins/*'   # add this
```

```jsonc
// package.json
"devDependencies": {
  "concurrently": "^9.0.0",
  "@opentelemetry/api": "^1.9.0"   // add this
}
```

then re-run `pnpm install`.

### Scaffolds created before `--apps`

They keep working unchanged: with no `apollo.apps.json` and no
`APOLLO_APPS_FILE`, the backend installs every app. To choose apps in one of
them, after `pnpm backend:update` has brought in a backend with
`scripts/apl.ts`:

1. Add the script to the root `package.json`:

   ```jsonc
   "apl": "pnpm --filter ./apps/backend exec bun run scripts/apl.ts"
   ```

2. Add `apollo.apps.json.apl-tmp` to `.gitignore`.
3. Run `pnpm apl apps`. Create `apollo.apps.json` at the project root listing
   every app it shows as `installed` — `{ "apps": ["<slug>", "<slug>"] }` —
   so nothing changes yet. Leave out the ones marked `mandatory`.
4. Add `APOLLO_APPS_FILE=../../apollo.apps.json` to the root `.env.local`.
   ⚠️ Do steps 3 and 4 in this order: without the variable, `apl` writes to
   `apps/backend/apollo.apps.json`, inside the submodule.
5. `pnpm apl doctor` should report nothing. Then `pnpm apl uninstall <slug>`
   for each app the project does not use — its tables and data are kept.

## Deploying to production

The scaffold pins `packageManager: "pnpm@11.0.0"` so Corepack picks the right
pnpm. If your CI / deploy host installs pnpm separately, **make sure it's
pnpm 11+** — pnpm 11 sets `strictDepBuilds: true` by default and reads the
allow-list from `allowBuilds` in `pnpm-workspace.yaml`. pnpm 10 silently
ignores `allowBuilds`; pnpm 11 silently ignores the deprecated
`onlyBuiltDependencies`. Mixing the two leads to `sharp` (and `esbuild`,
`@swc/core`, …) skipping their postinstalls, which surfaces at runtime as:

```
Failed to load external module sharp-<hash>:
  Error: Cannot find module 'sharp-<hash>'
```

The scaffold's `pnpm-workspace.yaml` already lists the binaries Apollo CMS
needs in `allowBuilds`. If you add a new native dep, append it there.
