Skip to content

Commit ecf83e1

Browse files
committed
feat: Adonis tenancy
0 parents  commit ecf83e1

498 files changed

Lines changed: 54700 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
pull_request:
7+
branches: [master, main]
8+
9+
jobs:
10+
lint-and-typecheck:
11+
name: Lint & Typecheck
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '24'
20+
21+
# Repo .npmrc sets `ignore-scripts=true` for local-dev safety. CI is a
22+
# controlled environment and tsx/esbuild rely on the postinstall fallback
23+
# to fetch the platform binary (`@esbuild/<os>-<arch>`) when optional-dep
24+
# resolution doesn't pick it up — so we re-enable scripts here.
25+
- name: Install dependencies
26+
env:
27+
npm_config_ignore_scripts: 'false'
28+
run: npm install --legacy-peer-deps --include=optional
29+
30+
- name: Typecheck
31+
run: npm run typecheck
32+
33+
- name: Build
34+
run: npm run build
35+
36+
- name: Test (unit)
37+
run: npm test
38+
39+
# Surfaces unused exports / orphaned files / unused deps. `--no-exit-code`
40+
# so the report is informational; flip to required once the baseline
41+
# is enforced.
42+
- name: Knip (unused-code report)
43+
run: npm run knip
44+
45+
# Production-only audit so we don't trip on devDependency-only CVEs that
46+
# never ship to consumers. `high` level matches the publish-gate bar.
47+
- name: npm audit (production deps)
48+
run: npm run audit:prod
49+
50+
test-integration:
51+
name: Integration Tests
52+
runs-on: ubuntu-latest
53+
54+
services:
55+
postgres:
56+
image: postgres:16-alpine
57+
env:
58+
POSTGRES_USER: postgres
59+
POSTGRES_PASSWORD: postgres
60+
POSTGRES_DB: multitenancy_test
61+
ports:
62+
- 5432:5432
63+
options: >-
64+
--health-cmd pg_isready
65+
--health-interval 5s
66+
--health-timeout 5s
67+
--health-retries 10
68+
69+
redis:
70+
image: redis:7.0.5-alpine
71+
ports:
72+
- 6379:6379
73+
options: >-
74+
--health-cmd "redis-cli ping"
75+
--health-interval 5s
76+
--health-timeout 5s
77+
--health-retries 10
78+
79+
env:
80+
TZ: UTC
81+
NODE_ENV: test
82+
HOST: 127.0.0.1
83+
PORT: 3333
84+
APP_KEY: a-32-character-long-secret-key!!
85+
LOG_LEVEL: error
86+
TENANT_HEADER_KEY: x-tenant-id
87+
DB_HOST: 127.0.0.1
88+
DB_PORT: 5432
89+
DB_USER: postgres
90+
DB_PASSWORD: postgres
91+
DB_DATABASE: multitenancy_test
92+
REDIS_HOST: 127.0.0.1
93+
REDIS_PORT: 6379
94+
QUEUE_REDIS_HOST: 127.0.0.1
95+
QUEUE_REDIS_PORT: 6379
96+
QUEUE_REDIS_DB: 1
97+
CACHE_REDIS_HOST: 127.0.0.1
98+
CACHE_REDIS_PORT: 6379
99+
CACHE_REDIS_DB: 2
100+
101+
steps:
102+
- uses: actions/checkout@v4
103+
104+
- name: Setup Node.js
105+
uses: actions/setup-node@v4
106+
with:
107+
node-version: '24'
108+
109+
- name: Install dependencies
110+
env:
111+
npm_config_ignore_scripts: 'false'
112+
run: npm install --legacy-peer-deps --include=optional
113+
114+
- name: Test (integration)
115+
run: npm run test:integration
116+
117+
test-e2e-demo:
118+
name: E2E (demo app)
119+
runs-on: ubuntu-latest
120+
121+
services:
122+
postgres:
123+
image: postgres:16-alpine
124+
env:
125+
POSTGRES_USER: app
126+
POSTGRES_PASSWORD: app
127+
POSTGRES_DB: lasagna_demo
128+
ports:
129+
- 55432:5432
130+
options: >-
131+
--health-cmd "pg_isready -U app -d lasagna_demo"
132+
--health-interval 5s
133+
--health-timeout 5s
134+
--health-retries 10
135+
136+
redis:
137+
image: redis:7-alpine
138+
ports:
139+
- 56379:6379
140+
options: >-
141+
--health-cmd "redis-cli ping"
142+
--health-interval 5s
143+
--health-timeout 5s
144+
--health-retries 10
145+
146+
mailcatcher:
147+
image: schickling/mailcatcher
148+
ports:
149+
- 1025:1025
150+
- 1080:1080
151+
152+
env:
153+
TZ: UTC
154+
NODE_ENV: development
155+
HOST: 127.0.0.1
156+
PORT: 3333
157+
APP_KEY: a-32-character-long-secret-key!!
158+
LOG_LEVEL: error
159+
TENANT_HEADER_KEY: x-tenant-id
160+
APP_DOMAIN: localhost
161+
DB_HOST: 127.0.0.1
162+
DB_PORT: 55432
163+
DB_USER: app
164+
DB_PASSWORD: app
165+
DB_DATABASE: lasagna_demo
166+
REDIS_HOST: 127.0.0.1
167+
REDIS_PORT: 56379
168+
QUEUE_REDIS_HOST: 127.0.0.1
169+
QUEUE_REDIS_PORT: 56379
170+
QUEUE_REDIS_DB: 1
171+
CACHE_REDIS_HOST: 127.0.0.1
172+
CACHE_REDIS_PORT: 56379
173+
CACHE_REDIS_DB: 2
174+
BACKUP_STORAGE_PATH: ./storage/backups
175+
DEMO_ADMIN_TOKEN: demo-admin-token-change-me
176+
MAILCATCHER_HOST: 127.0.0.1
177+
MAILCATCHER_PORT: 1025
178+
MAIL_FROM_ADDRESS: demo@example.test
179+
MAIL_FROM_NAME: Demo Multitenancy
180+
181+
steps:
182+
- uses: actions/checkout@v4
183+
184+
- name: Setup Node.js
185+
uses: actions/setup-node@v4
186+
with:
187+
node-version: '24'
188+
189+
# `pg_dump` / `pg_restore` / `psql` on PATH so backups_real.spec.ts +
190+
# the import / clone tests run instead of skipping. The suite skips
191+
# gracefully when these are absent, but we want them green in CI.
192+
- name: Install postgresql-client
193+
run: |
194+
sudo apt-get update
195+
sudo apt-get install -y --no-install-recommends postgresql-client
196+
197+
- name: Install dependencies
198+
env:
199+
npm_config_ignore_scripts: 'false'
200+
run: npm install --legacy-peer-deps --include=optional
201+
202+
# Demo's `@adonisjs-lasagna/saas-tenancy` is a workspace symlink that
203+
# resolves through `build/`, so a fresh build is mandatory before the
204+
# e2e suite imports from it.
205+
- name: Build package
206+
run: npm run build
207+
208+
- name: Backoffice setup (creates backoffice schema + tenants table)
209+
working-directory: examples/api
210+
run: npx tsx ace.ts backoffice:setup
211+
212+
- name: Run e2e suite
213+
working-directory: examples/api
214+
run: npx tsx ace.ts test e2e

.github/workflows/docs.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Deploy docs site
2+
3+
on:
4+
push:
5+
branches: [master]
6+
paths:
7+
- 'docs/**'
8+
- '.github/workflows/docs.yml'
9+
- 'package.json'
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
# Don't pile up parallel deploys — cancel in-progress runs when a new
18+
# commit lands, but let an in-flight publish finish (skip-then-queue).
19+
concurrency:
20+
group: pages
21+
cancel-in-progress: false
22+
23+
jobs:
24+
build:
25+
name: Build VitePress site
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
with:
30+
fetch-depth: 0 # lastUpdated needs full git history
31+
32+
- uses: actions/setup-node@v4
33+
with:
34+
node-version: '24'
35+
cache: npm
36+
37+
# Repo .npmrc disables postinstall scripts for safety; CI is a
38+
# controlled env where esbuild's platform binary fetch is needed.
39+
- name: Install dependencies
40+
env:
41+
npm_config_ignore_scripts: 'false'
42+
run: npm install --legacy-peer-deps --include=optional
43+
44+
- name: Build site
45+
run: npm run docs:build
46+
47+
- uses: actions/configure-pages@v5
48+
49+
- uses: actions/upload-pages-artifact@v3
50+
with:
51+
path: docs/.vitepress/dist
52+
53+
deploy:
54+
name: Deploy to GitHub Pages
55+
needs: build
56+
runs-on: ubuntu-latest
57+
environment:
58+
name: github-pages
59+
url: ${{ steps.deployment.outputs.page_url }}
60+
steps:
61+
- id: deployment
62+
uses: actions/deploy-pages@v4

.github/workflows/publish.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Publish Package
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
publish:
10+
name: Build & Publish
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
id-token: write
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Setup Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '24'
23+
registry-url: 'https://registry.npmjs.org'
24+
25+
# Repo .npmrc sets `ignore-scripts=true` for local-dev safety; CI needs
26+
# esbuild's postinstall to run so the platform binary lands on disk.
27+
- name: Install dependencies
28+
env:
29+
npm_config_ignore_scripts: 'false'
30+
run: npm install --include=optional
31+
32+
- name: Typecheck
33+
run: npm run typecheck
34+
35+
- name: Build
36+
run: npm run build
37+
38+
# Prerelease versions (anything after a `-`, e.g. `0.2.0-beta.1`) are
39+
# published under the `next` dist-tag so they NEVER override `latest`.
40+
# Stable versions get the default `latest` tag.
41+
- name: Resolve npm dist-tag
42+
id: tag
43+
run: |
44+
VERSION=$(node -p "require('./package.json').version")
45+
if [[ "$VERSION" == *-* ]]; then
46+
echo "name=next" >> "$GITHUB_OUTPUT"
47+
else
48+
echo "name=latest" >> "$GITHUB_OUTPUT"
49+
fi
50+
echo "Publishing $VERSION under dist-tag $(grep '^name=' $GITHUB_OUTPUT | tail -1 | cut -d= -f2)"
51+
52+
- name: Publish to npm
53+
run: npm publish --access public --tag ${{ steps.tag.outputs.name }}
54+
env:
55+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules/
2+
build/
3+
.env
4+
*.log
5+
.claude
6+
CLAUDE.md
7+
cmultitenancy-packagetestsunitadmin
8+
# VitePress build output (the markdown under docs/ IS tracked)
9+
docs/.vitepress/cache/
10+
docs/.vitepress/dist/

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignore-scripts=true

CHANGELOG.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Changelog
2+
3+
All notable changes to `@adonisjs-lasagna/saas-tenancy` are documented here.
4+
5+
This project adheres to [Semantic Versioning](https://semver.org/).
6+
7+
---
8+
9+
## [0.1.0] — 2026-05-07
10+
11+
Initial release of `@adonisjs-lasagna/saas-tenancy`.
12+
13+
This package continues the work previously published as
14+
`@adonisjs-lasagna/multitenancy` v2.x. The rename reflects the
15+
positioning of the package as the SaaS-tenancy foundation for
16+
AdonisJS 7. The codebase is the same hardened core: pluggable
17+
isolation drivers (schema-pg, database-pg, rowscope-pg, sqlite-memory),
18+
13 typed lifecycle events, eight satellite features, contextual
19+
logging, scheduled backups, read-replica routing, the `tenant:doctor`
20+
diagnostic command, and an admin REST API.
21+
22+
### Highlights
23+
24+
- **Schema isolation** — every tenant gets its own `tenant_<uuid>`
25+
PostgreSQL schema, provisioned and routed automatically.
26+
- **Pluggable isolation** — schema-per-tenant, database-per-tenant,
27+
shared-with-row-scope, or in-memory SQLite for tests.
28+
- **Lifecycle hooks + 13 typed events** — declarative `before` /
29+
`after` hooks wired into commands and jobs.
30+
- **Contextual logging**`tenantId` rides through HTTP and queue
31+
jobs via `AsyncLocalStorage`.
32+
- **`tenant:doctor`** — ten built-in checks, `--fix` for auto-recovery,
33+
`--json` for CI, `--watch` for a live TUI.
34+
- **Plans & quotas** — declarative plans, atomic rolling counters,
35+
snapshot usage, `enforceQuota()` middleware that returns 429.
36+
- **Scheduled backups + retention** — tier-based intervals, S3 mirror
37+
with purge awareness.
38+
- **Health probes + Prometheus**`/livez`, `/readyz`, `/healthz`,
39+
`/metrics`. No `prom-client` peer dep.
40+
- **Read replica routing** — round-robin, random, or sticky-by-tenant-id.
41+
- **REST admin API** — 36 endpoints + OpenAPI 3.1 spec + Swagger UI.
42+
- **Soft delete TTL**`--keep-schema` on destroy,
43+
`tenant:purge-expired` on a cron.
44+
- **Eight satellites** — audit logs (append-only at SQL level),
45+
webhooks (HMAC-signed + retries), quotas, feature flags, branding,
46+
SSO/OIDC, metrics, impersonation. All optional.
47+
48+
### History
49+
50+
Pre-rename history (v1.x and v2.0.0-beta.x of
51+
`@adonisjs-lasagna/multitenancy`) lives at the prior repository:
52+
[github.com/Arcoders/Adonisjs-Lasagna-Multitenancy](https://github.com/Arcoders/Adonisjs-Lasagna-Multitenancy).

0 commit comments

Comments
 (0)