Private enterprise app via git submodule
This guide sets up apps/enterprise/ as a private-only app: its source
lives in a separate private repo and is mounted into the public monorepo as a
git submodule. The public repo only ever stores a commit pointer — never the
source.
Why submodule (not a private npm registry)? The
@lumibase/*packages areprivate: trueand consumed viaworkspace:*. A submodule keeps that intact: when checked out, the enterprise app is just anotherapps/*workspace.
As configured in this repo
This is already wired up (.gitmodules → submodule apps/enterprise):
- Private repo:
https://github.com/lumibase-ai/enterprise-core.git - Tracked branch:
feat/custom-domains-docs(this repo's default branch — it has nomain) - Path:
apps/enterprise/(matched by theapps/*workspace glob)
Verified behaviour when the submodule is absent (public checkout / CI):
pnpm install --frozen-lockfile succeeds and simply skips the empty
apps/enterprise/ directory (workspace drops from 18 → 17 projects, no error).
So actions/checkout@v5 with its default submodules: false needs no change —
public CI installs cleanly without ever touching the private repo.
Sections 1–2 below are the generic recipe (already executed for this repo); keep them for reference or when re-homing the submodule.
Threat model / boundary rules
- One-way dependency:
enterprise → coreonly. Core code (cms,studio,packages/*) must neverimport '@lumibase/enterprise', or the public build breaks (the submodule is absent in public checkouts). .gitmodulesis committed to the public repo and exposes the private repo URL. That is acceptable — a URL is not a credential; cloning still requires repo access.- Public CI must not fetch the submodule.
actions/checkoutdefaults tosubmodules: false— keep it that way for any public workflow. Only the private/self-hosted build job fetches it (with a token).
1. Create the private repo
Create an empty private repo, e.g. your-org/lumibase-enterprise. Move the
scaffolded apps/enterprise/ contents into it as the repo root:
# from a fresh clone of the (empty) private repo
git clone git@github.com:your-org/lumibase-enterprise.git
cd lumibase-enterprise
# copy the scaffold that was generated in the monorepo:
cp -R /path/to/lumibase/apps/enterprise/. .
git add -A && git commit -m "feat: initial enterprise app"
git push origin main
2. Add it back into the monorepo as a submodule
In the public monorepo, replace the local scaffold with the submodule:
# remove the local copy first (it now lives in the private repo)
git rm -r --cached apps/enterprise
rm -rf apps/enterprise
# mount the private repo at the same path
git submodule add git@github.com:your-org/lumibase-enterprise.git apps/enterprise
git commit -m "chore: mount enterprise app as private submodule"
This creates .gitmodules:
[submodule "apps/enterprise"]
path = apps/enterprise
url = git@github.com:your-org/lumibase-enterprise.git
3. Cloning behaviour
# Contributors WITHOUT access (public-only):
git clone git@github.com:your-org/lumibase.git
# → apps/enterprise/ is an empty pointer. pnpm install ignores it
# because there is no package.json inside. Public build works.
# Team WITH access (full build):
git clone --recurse-submodules git@github.com:your-org/lumibase.git
# or, after a plain clone:
git submodule update --init --recursive
# → apps/enterprise/ is populated; pnpm picks it up as a workspace.
pnpm-workspace.yaml already globs apps/* and turbo.json uses task globs,
so no config changes are needed in either case.
4. Public CI — must NOT fetch the submodule
In every public workflow that runs actions/checkout, leave submodules off
(the default). Be explicit to prevent accidents:
- uses: actions/checkout@v4
with:
submodules: false # never fetch private submodules in public CI
Public pnpm install / turbo run build then simply skip the empty
apps/enterprise/ directory.
5. Private / self-hosted build job (fetches the submodule)
This job runs in the private repo (or a protected workflow with a secret).
Use a deploy token / fine-grained PAT with read access to
lumibase-enterprise:
# .github/workflows/enterprise-deploy.yml (private)
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.ENTERPRISE_SUBMODULE_TOKEN }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 24 }
- run: pnpm install --ignore-scripts
- run: pnpm --filter @lumibase/enterprise typecheck
- run: pnpm --filter @lumibase/enterprise test
- run: pnpm --filter @lumibase/enterprise build
- run: pnpm --filter @lumibase/enterprise deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
6. Day-to-day: updating the pointer
# pull latest enterprise code into the submodule
git submodule update --remote apps/enterprise
git add apps/enterprise
git commit -m "chore: bump enterprise submodule"
7. Enforce the one-way boundary (recommended)
Add a public CI guard so nobody accidentally imports the enterprise app from core code:
# fails if any core source imports @lumibase/enterprise
! grep -rES "from ['\"]@lumibase/enterprise" \
apps/cms apps/studio packages \
|| { echo "core must not import @lumibase/enterprise"; exit 1; }
8. Cloudflare Pages (and other Git-integration builders)
⚠️ Gotcha that will break your Pages deploys. Cloudflare Pages' Git integration clones submodules by default when it builds. It has no credentials for the private
enterprise-corerepo, so every Pages project fails at the Building stage — verified:lumibase-docs,lumibase-landing, andlumibase-marketplaceall flipped success → failure at the exact commit that introduced the submodule, and were green on its parent.
Fix — a dashboard toggle, not a repo file (there is no wrangler.toml /
.cloudflare setting for this). For each affected Pages project:
- Cloudflare Dashboard → Workers & Pages → select the project.
- Settings → Builds & deployments (a.k.a. Build configuration).
- Disable "Include submodules when cloning".
- Save, then Retry deployment on the failed build.
This is safe because none of the Pages apps import @lumibase/enterprise
(guard it the same way as §7 if you want CI to enforce it):
# fails if any Pages app references the enterprise submodule
! grep -rES "@lumibase/enterprise|apps/enterprise" \
apps/docs apps/landing apps/marketplace \
|| { echo "Pages apps must not reference apps/enterprise"; exit 1; }
The same applies to any external builder that clones via Git integration (Vercel, Netlify, Amplify): either disable submodule fetching or give it a read token for the private repo.
Recap
| Concern | Public repo | Private repo / team |
|---|---|---|
| Source visible? | No (empty pointer) | Yes |
pnpm install | Skips empty dir | Treats as workspace |
| CI checkout | submodules: false | submodules: recursive + token |
| Cloudflare Pages | Disable "Include submodules when cloning" | n/a |
| Deploy | n/a | wrangler deploy --env production |