Package: tenancy
Generated from api/packages/tenancy
Source: api/packages/tenancy. The sections after the README are extracted from the code on every docs build.
README
Section titled “README”What does it do
Section titled “What does it do”Provisions and removes tenants of a multi-domain ulams API. One API deployment serves the
platform (api.localhost) and any number of tenants, each on its own host
(<slug>.localhost) with its own:
- PostgreSQL role and database (
ulams_<slug>), - MinIO/S3 bucket with public read (
ulams-<slug>), .env.<host>file and storage directory (gecche/laravel-multidomain),APP_KEYand Passport key pair (a token from one tenant is rejected by every other),- Redis key prefix for queues, cache and Horizon (
ulams_<slug>_), - demo users and the public settings the front reads (
global.companyName,global.frontURL,theme.theme,theme.accent), - optionally demo mode (
tenants.demo→DEMO_MODEin the env file, see packages/demo).
The registry of tenants is the tenants table in the platform database. Secrets in it
(db_password, app_key, Passport keys) are encrypted with the platform APP_KEY.
It also stops laravel-multidomain from serving unknown hosts with the platform .env: a
global middleware answers 404 to any host that is neither a platform host
(TENANCY_PLATFORM_HOSTS) nor a registered tenant with its own env file.
Commands
Section titled “Commands”All commands run on the platform (no --domain).
# create or resume; finished steps are skippedphp artisan ulams:tenant:create coffee --name="The Coffee Atlas" --theme=coffee --accent="#C2552D" --users=5
# run one step againphp artisan ulams:tenant:create coffee --redo=migrate
# demo mode (packages/demo): DEMO_MODE=true in the env file, login without password,# hourly reset; only the env file is rewrittenphp artisan ulams:tenant:create coffee --demo=on
php artisan ulams:tenant:listphp artisan ulams:tenant:list --hosts # active API hosts, one per line
# rebuild env files, registrations and Passport keys from the tenants table (init.sh)php artisan ulams:tenant:sync-env --migrate
# AI settings (ANTHROPIC_API_KEY, AI_*) are inherited from the platform env; per-tenant override (ADR 0063)php artisan ulams:tenant:set-env acme --set=AI_DRIVER=disabled
# drop database, role, bucket, env file, storage directory and Redis keysphp artisan ulams:tenant:delete coffee --forcePlatform API (off by default)
Section titled “Platform API (off by default)”/api/platform/tenants and /api/platform/operations manage tenants over HTTP (ADR 0078, 0085). They exist only on a
platform host with TENANCY_PLATFORM_API=true (404 otherwise), for users with the platform_admin permission and
scoped tokens with platform:read|write. Creation and deletion are queued jobs (ProvisionTenantJob,
DeleteTenantJob) that record each step in tenant_operations; TenantLifecycle is the code the artisan commands
and the API share. CLI: ulams tenants …. Operator guide: the docs site, “Platform tenant API”.
Steps of ulams:tenant:create, recorded in tenants.steps:
| Step | What it does |
|---|---|
database |
CREATE ROLE + CREATE DATABASE through the pgsql_admin connection |
bucket |
creates the bucket and a public read policy (AWS SDK) |
env |
domain:add <slug>.localhost with the tenant values |
migrate |
migrate --force |
passport_keys |
passport:keys, copy stored encrypted on the tenant row |
passport_client |
passport:client --personal |
permissions |
db:seed --class=PermissionsSeeder (creates the admin user) |
lti_keys |
ulams:lti:rotate-keys --init: the tenant’s LTI signing keys (ADR 0012) |
demo |
ulams:tenant:seed-demo: tutor, students, settings |
Every step from migrate on runs as php artisan … --domain=<host> in a child process.
An in-process Artisan::call would keep the platform configuration.
Demo users: admin@<slug>.ulams.app, tutor@<slug>.ulams.app,
student1@<slug>.ulams.app … studentN@<slug>.ulams.app, all with the password from
TENANT_DEMO_PASSWORD (default secret, development only).
Configuration
Section titled “Configuration”See src/config.php and the tenancy section of docs/enviromental-variables.md.
vendor/bin/phpunit --testsuite tenancyThe end-to-end isolation test provisions two real tenants and is opt-in:
docker compose exec -T -e TENANCY_INTEGRATION=1 api vendor/bin/phpunit packages/tenancy/tests/IntegrationAPI endpoints
Section titled “API endpoints”| Method | Path | Auth | Action |
|---|---|---|---|
GET |
/api/platform/tenants |
yes | PlatformTenantController@index |
POST |
/api/platform/tenants |
yes | PlatformTenantController@store |
GET |
/api/platform/tenants/{slug} |
yes | PlatformTenantController@show |
PATCH |
/api/platform/tenants/{slug}/env |
yes | PlatformTenantController@env |
DELETE |
/api/platform/tenants/{slug} |
yes | PlatformTenantController@destroy |
GET |
/api/platform/operations |
yes | PlatformOperationController@index |
GET |
/api/platform/operations/{id} |
yes | PlatformOperationController@show |
Permissions
Section titled “Permissions”None.
Settings
Section titled “Settings”Registered with AdministrableConfig::registerConfig (editable in the admin panel under Configuration → Settings).
None.
Events
Section titled “Events”None.
Artisan commands
Section titled “Artisan commands”| Command | Description | Signature |
|---|---|---|
ulams:db:recreate-views |
Recreate the searchable_events SQL view with the current model class names | ulams:db:recreate-views |
ulams:h5p:export-config |
Write the least-privilege env files and Passport public keys the H5P service mounts in production (H5P_SERVICE_CONFIG_DIR) | ulams:h5p:export-config |
ulams:tenant:create |
Provision a tenant (database, bucket, env file, migrations, keys, demo users). Safe to re-run: finished steps are skipped. | ulams:tenant:create {slug : Lowercase letters and digits, used for hosts, database and bucket names} {--name= : Display name (APP_NAME and the global.companyName setting)} {--theme= : Front theme preset key, coffee, oncall, nightsky, gravity, poland or ulam} {--accent= : Accent colour, e.g. #C2552D} {--users=5 : Number of demo students} {--demo= : Demo mode (DEMO_MODE: login without password, hourly reset): on or off} {--db-password= : Password of a database you created yourself (TENANCY_DATABASE_PROVISIONER=manual), at least 16 characters} {--redo=* : Step to run again even if recorded as done: database, bucket, env, migrate, passport_keys, passport_client, permissions or demo} |
ulams:tenant:delete |
Delete a tenant and all its data | ulams:tenant:delete {slug} {--force : Required. Drops the database, role, bucket objects, env file and storage} |
ulams:tenant:list |
List provisioned tenants | ulams:tenant:list {--hosts : Print only the API hosts of active tenants, one per line} |
ulams:tenant:schedule-loop |
Run the scheduler of this domain every minute in one long-lived process | ulams:tenant:schedule-loop {--max-time=3600 : Exit after this many seconds (the supervisor starts it again with fresh code and config)} {--once : Run one scheduler tick now and exit} {--lock : With --once, claim the minute with the shared lock first (workers.sh lean mode: one process ticks every domain each minute)} |
ulams:tenant:seed-demo |
Create demo users and public settings for the current tenant (internal step of ulams:tenant:create) | ulams:tenant:seed-demo {--users=5 : Number of students} {--name= : Display name, stored as global.companyName} {--theme= : Front theme preset key, stored as theme.theme} {--accent= : Accent colour, stored as theme.accent} {--front-url= : Stored as global.frontURL} {--email-domain= : Domain of the demo e-mail addresses} |
ulams:tenant:set-env |
Override or reset inheritable platform settings (AI key, driver, models) for one tenant | ulams:tenant:set-env {slug} {--set=* : KEY=value override for this tenant (inheritable settings only, e.g. AI_DRIVER=fake)} {--unset=* : KEY to drop, so the tenant inherits the platform value again} |
ulams:tenant:sync-env |
Rebuild .env. |
ulams:tenant:sync-env {--migrate : Also run pending migrations for every tenant} |
ulams:tenant:work-once |
Run the scheduler tick and drain a queue once, then exit (for cron instead of long-lived workers) | ulams:tenant:work-once {--queue=default,broadcast,video : Comma-separated queues to drain, highest priority first} {--connection= : Queue connection (default: the default connection; builder and long jobs use <driver>-builder and <driver>-long-job)} {--timeout=60 : Seconds a single job may run (builder jobs run up to 1800, long jobs up to 18000)} {--max-time=50 : Stop taking new jobs after this many seconds} {--memory=256 : Memory limit of the worker in MB} {--schedule : Run one scheduler tick before the queue (only on the line that runs every minute)} |
ulams:upgrade |
Bring the platform and every tenant to the current release: migrations, permissions, keys and one-off data steps | ulams:upgrade {--tenant=* : Only these tenant slugs (the platform is then left out)} {--platform-only : Only the platform} {--force-step=* : Run this one-off step again even if it already ran} {--dry-run : List what would run; change and record nothing} |
Scheduled jobs
Section titled “Scheduled jobs”None.
Environment variables read
Section titled “Environment variables read”AI_DRIVER, AI_MODEL_DEFAULT, AI_MODEL_DEFAULT_LABEL, AI_MODEL_LIGHT, AI_MODEL_LIGHT_LABEL, AI_MODEL_PREMIUM, AI_MODEL_PREMIUM_LABEL, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, AWS_USE_PATH_STYLE_ENDPOINT, H5P_SERVICE_CONFIG_DIR, INITIAL_USER_EMAIL, INITIAL_USER_PASSWORD, TENANCY_ADMIN_CONNECTION, TENANCY_ADMIN_HOST, TENANCY_API_HOST, TENANCY_BUCKET, TENANCY_BUCKET_PUBLIC_URL, TENANCY_CONTENT_HOST, TENANCY_DATABASE, TENANCY_DATABASE_PROVISIONER, TENANCY_EMAIL_DOMAIN, TENANCY_ENFORCE_HOSTS, TENANCY_FRONT_HOST, TENANCY_NEW_SITES, TENANCY_PHP_BINARY, TENANCY_PLATFORM_API, TENANCY_PLATFORM_HOST, TENANCY_PLATFORM_HOSTS, TENANCY_PROCESS_TIMEOUT, TENANCY_REDIS_PREFIX, TENANCY_S3_ENDPOINT, TENANCY_S3_KEY, TENANCY_S3_PUBLIC_READ_POLICY, TENANCY_S3_REGION, TENANCY_S3_SECRET, TENANCY_SCHEDULER_LOCK, TENANCY_SCHEME, TENANCY_STORAGE_PUBLIC_URL, TENANT_DEMO_PASSWORD, TENANT_SLUG