# Atrium

**Tutelora Atrium** (short name **Atrium**), built on the Tutelora starter pack. Started 6 Nov 2026.

The one internal app that does, once, the jobs every Tutelora product would otherwise rebuild. Its main users are other
apps; people meet it only to sign in and to open short links, and never see the name. What it is and why:
`reji/Tutelora Atrium Shared Platform Services.docx`, read with `reji/docs/spec-amendments.md`, which overrides it where they differ. How it gets built: the
master plan, `reji/plans/2026-11-06-M00-atrium-master-plan.md`, and the identity architecture, `reji/plans/2026-11-06-IA-identity-architecture.md`.

The standard is the **Tutelora Engineering Handbook** (`/Users/reji/dev/Tutelora/Tutelora_Engineering_Handbook.html`).
The house rules and the family rules apply.

## How we build

**Every module, the foundation included, is discussed and its mockups planned before any of it is built.** The gate, in order:
discussion → plan (`reji/plans/`) → mockups (`reji/ui/html/<plan>/`) → review → build → test → manual journey. Each is approved
separately, and a finished module opens the next discussion, not the next build (the Architect, 6 November 2026). The master plan's
§1 has the standing policies and §6 the agenda and mockups for each module.

## The modules

| Module | Codename | ID, in code, config and API paths | Plan | Shown to users? |
|---|---|---|---|---|
| The foundation | · | · | AF | No |
| Accounts | Clavis | `accounts` | CL | As "Tutelora account" |
| Organisations | Album | `organisation` | AF, CL | No |
| Subscriptions and billing | Tabula | `subscription` | AF, TB | No |
| E-mail, SMS, WhatsApp | Nuntius | `notify` | NS, NU | No |
| Short links | Tutelink | `links` | TL | Yes |
| Voucher and access codes | Tessera | `vouchers` | TE | No |
| AI assistant | Tutela | `tutela` | TU | Yes |
| Payments | Aerarium | `payments` | AE | No |
| Reading documents | Lector | `ocr` | LE | No |

## The starter pack

- **Started from:** TuteCore 2.1.1 (6 Nov 2026). <!-- brand-guard: allow -->
- **Ported up to:** 2.1.1. A newer release is taken by a port plan, `reji/plans/<date>-CORE-port-<version>.md`
  (handbook 13.3); `php artisan core:status` lists what differs from the version claimed here (`config/core.php`).
- **This work doesn't change the starter pack** (the Architect, 6 November 2026). A change to a foundation module is made here and listed under
  Exceptions, and isn't lifted back. The plans the inherited docs cite (CORE, SN, SP, WA) stayed behind in the starter pack's repository,
  `Tutelora/tutecore/reji/plans`. <!-- brand-guard: allow -->

## Exceptions

What this app does differently from the handbook or the starter pack, on purpose. A difference not listed here is drift.

| Rule | What Atrium does | Why | Since |
|---|---|---|---|
| Handbook 13.3: a foundation change is made in the starter pack first | Atrium makes them here and doesn't change the starter pack | The Architect, 6 November 2026: this work concerns Atrium alone | 6 Nov 2026 |
| Starter pack: one worker on the database queue (slice C1, AF-5) | Redis through `predis`, with pools `default`, `bulk`, `ai` and `ocr` (`config/queue.php`), `retry_after` 1860 seconds, and a backoff on every job that tries more than once. Long jobs moved to `bulk`. Changes `app:agents`, `queue.plist`, the production environment, `Readiness`, `Dashboard`, `lang/en/operations.php`, `config/database.php`, the seven long jobs, `ScanFile`, `SendEmailMessage`, and three tests | The spec's VPS case is built on Redis, and a long import shouldn't hold up an e-mail | 6 Nov 2026 |
| Starter pack: `Idempotent` looks the key up, runs, then stores (slice C2) | The key is reserved first, so two copies can't both run; `idempotency_key`'s answer columns may be empty while it runs. Changes `Idempotent`, `IdempotencyKey`, `lang/en/api.php`, and a test | Atrium's products retry over the network, and these requests send SMS and spend vouchers | 6 Nov 2026 |
| Starter pack: no outbound webhooks (slice C3; handbook 12.9 foresaw them) | Signed webhooks on the event catalogue. Changes `NotificationEvent`, `EventCatalogue`, `Notify`, `WorkspaceStatusChanged`, `LogPurger`, `lang/en/log.php`, two guard lists and `PurgeTest` | Products must hear of what happens here | 6 Nov 2026 |
| Starter pack: line logs, and `/up` proves only that PHP booted (slice C4) | Request IDs through logs, jobs and outbound calls; JSON logs in production; `/up` proves the database and the queue. Changes `bootstrap/app.php`, `AppServiceProvider`, `config/logging.php`, the production environment | A shared service needs one problem followed across apps, and a load balancer needs an honest answer | 6 Nov 2026 |

| The starter pack's name and words (slice A1) | The product is Atrium on the console (`brand.in_console`), and the company everywhere else, mail included; an organisation is called one (`BRAND_TENANT`). The session cookie carries no product's name. Changes `config/brand.php`, `config/session.php`, `brand.json`, and a test | People at `id` never see "Atrium" (plan AF §4) | 6 Nov 2026 |
| Handbook 12.6: `/api/v1`, and every route answers on every host (slice A2, AF-1, AF-3) | `/v1` on the API host; `/console` for the Service, also on the API host; people on `id`; only the short-link page on the short-link host. Enforced by `EnforceHost` and `ConsoleDoor`, not `Route::domain` alone, because the sign-in serves two hosts under one name. Starter-pack addresses move from `/platform` to `/console`; route names stay `platform.*` | The spec's hosts, and `tutel.ink` mustn't serve a sign-in | 6 Nov 2026 |
| Starter pack: an organisation's public ID is a plain ULID (slice A3; plan IA §5) | Typed: `org_…`, `ent_…`, `ins_…`, so an API can't take one kind for another. `tenant` also gains its kind, address and registration, and no column naming a product | The 2022 lesson in plan IA | 6 Nov 2026 |
| Handbook 12.6 and 1.2: API access is a token from a membership, and the tenant never comes from a request (slice A4, AF-1, AF-2) | A `server` holds hashed keys with module scopes and allowed addresses, and names its organisation in `X-Organisation`, checked against a live instance of its product before anything is read. Idempotency keys are held by the server where a server called | Atrium's callers are software. Only a server of a product that organisation has running may name it | 6 Nov 2026 |
| Handbook 0.1: no microservices (AF-1) | Atrium is one monolith, but other products now depend on it over the network | The spec's purpose | 6 Nov 2026 |
| Starter pack: `/` is the Service's home, and its menu is flat (slice A6) | `/` sends the Service with no lens to `/console`; the menu is the console's pages, then the starter pack's with its logs folded into Records. `platform/Home.vue` is gone, the Workspaces page stays off the menu, and the shell gains a section label (with its type in `resources/js/types/index.ts`, added 7 November with CL2 when the type check caught it missing). `tenant` gains `known_as` beside its three-letter `short_name` | The approved mockups | 7 Nov 2026 |
| Starter pack: an account is an e-mail and a password inside a workspace, and signs in with both on one page (plan CL, slices CL1 and CL2) | The account (`user`, `acc_…`) holds its phones, e-mails and username as `account_identifier` rows, each verified one on one account, with `user.email` kept as a copy of the primary e-mail by the database; it needs no e-mail, password or organisation, and a membership lets it in through a verified identifier or a claim. Sign-in at `id` is one field in two steps: a phone, an e-mail or a username, read by its shape (`SignInName`), then what follows from it: a code for a phone, by Nuntius, with its waits and a pause kept in the database (`SignInCodes`, `sign_in_code`, `sign_in_pause`), or a password. Fortify's username is `login`, found by `AccountUserProvider` (the `accounts` driver in `config/auth.php`). Atrium is the products' OIDC provider (CL3): Passport 13.10, its own provider not discovered (`composer.json`), registered as `OidcServiceProvider` in `bootstrap/providers.php`, with its tables singular; `ResolveTenant` opens `oidc.*`, `EnforceHost` keeps `/oauth/*` and `/.well-known/*` to `id`, the step-up page names a product asking for the password, and removing or turning off a membership raises `membership.ended`. Joining at `id` (CL4) makes an organisation with its owner and a product on Free (`JoinOrganisation`); an organisation records its region and district (Input Fields `region`, `district`), a product its domain and the kinds it serves (`product_kind`); invitations go by text when a membership holds only a phone, accepted by a code; and a `billing` standard role joins owner, admin and member. An organisation's own web addresses (CL5, `organisation_domain`) open one product each once their CNAME is found, join that product's return addresses, and are told to it by webhook; a `domain.manage` permission, held by admins, and a Settings page manage them. At `id` (CL6) people have their own menu (Home, their organisation's pages, then their own), Home is a launchpad that also serves an account with no organisation (`ResolveTenant`), "How you sign in" manages the account's own identifiers (`aid_…` public IDs), and an account with no e-mail is mailed nothing (`Recipient::forUser`); Devices lists its sessions, and signing out everywhere tells every product it signed into (`account.signed_out` with `everywhere`); an organisation's products page adds a product (`product.add`); and a school-made account (`user.made_by_tenant_id`, `MakeAccount`) is claimed by a code or folded into its holder's own (`MergeAccounts`, `account_merge`, `account.merged`). Passkeys (CL7) use laravel/passkeys behind Atrium's own routes and limiters, in a singular `passkey` table, and count as more than a code; Google and Microsoft (Socialite, as Campus) link to an account by a verified e-mail it already holds and never make one (`account_link`). The `accounts` module (CL8) gives products `/v1/account/*`: adding a person naming the admin who acted, an account's own shared details, and the 15-minute recheck (`ended_session`); `account.changed` tells them of a change; a product's Service signs in there, never through `id`. Changes `User`, `Membership`, `Person`, `PersonMatcher`, `MergePeople`, `SplitPerson`, `AcceptInvitation`, `ProfilePart`, `SignInPart`, `LogPurger`, `FortifyServiceProvider`, `RefuseLockedAccount`, `ApplyRememberRule`, `SignInPages`, `Limiters`, `config/auth.php`, `config/fortify.php`, `routes/auth.php`, `SignIn.vue`, `IdleWatch.vue`, `lang/en/auth.php`, `ResolveTenant`, `EnforceHost`, `StepUpController`, `ConfirmPassword.vue`, `RemoveMembership`, `SetMembershipActive`, `EventCatalogue`, `bootstrap/providers.php`, `composer.json`, `Tenant`, `InvitePerson`, `AcceptInvitation`, `Invitation`, `InvitationController`, `routes/web.php`, `StandardRoles`, `lang/en/role.php`, `UpdateWorkspace`, `WorkspaceProfileController`, `WorkspaceFields.vue`, `Workspace.vue`, `Wizard.vue`, `InputFields\Catalogue`, `Permissions`, `WorkspaceSettingController`, `routes/setting.php`, `routes/console.php`, `Nav`, `HomeController`, `Home.vue`, `lang/en/home.php`, `lang/en/nav.php`, `lib/format.ts`, `Recipient` and the account mails, `AuthEvents`, `User` (a passkey user), `package.json`, `config/modules.php`, `AccountController` (name), and the tests that sign in | Clavis: one account for every product, signed in with whatever it holds (CL-1 to CL-3) | 7 Nov 2026 |
| Starter pack: security headers without a Content Security Policy; files open inline on the app's host; step-up by the password alone (plan SEC, slices SEC1 to SEC4) | A nonce-based Content Security Policy on every page (`SecurityHeaders`, `ContentSecurityPolicy`, `app.blade.php`, `brand-tokens.blade.php`, `app.ts`, SweetAlert2 without its style injection in `dialog.ts` and `vue-shims.d.ts`), `Permissions-Policy` and COOP; files sandboxed, PDFs downloaded on the app's hosts, and a files host by signed address (`FileResponse`, `FileController`); a fresh proof before an organisation's addresses, products or people change (`routes/person.php`, `routes/import.php`), with a code step-up for an account with no password (`StepUp.vue`, `HandleInertiaRequests`); the e-mail log's lazy load fixed (`EmailLogList`); the products' reach over accounts, server keys' modules, revoking and switching off, and the console's host sending no codes (SEC4); and the tests that follow (`FileRouteTest`, `ImportTest`, `EngineTest`) | `id` signs people in to every product, so its assessment's Priority 1 had to be met before it is deployed | 8 Nov 2026 |
| Starter pack: mail leaves only through the notifications engine, and `email_message` is the only mail log (plan NU, NU5) | Products' e-mail goes through Nuntius's `ProductMail`, the one other door to the mailer, named in `NoHardcodedMailTest`; `config/mail.php` gains `ses_smtp`, SES through its SMTP interface; and the products' e-mail log is a personal-data part (`PersonalData::PARTS`, `ProductEmailPart`), emptied by an erasure as the e-mail log is | Products send their own words from their own addresses along a chain of mailers, which Atrium's own event mail never does | 9 Nov 2026 |
| Starter pack: an append-only log is purged or kept for good, and the shell's styles are the sign-in shell's (plan NU, NU6) | `send_block_line`, why an address isn't written to, is kept for good (`LogGuardTest`'s list), since a suppression list must outlive what it suppresses; `shell.css` gains the unsubscribe page and a quiet shell for pages a person with no account opens | A parent who unsubscribed must never be written to again, and has no account to sign in with | 9 Nov 2026 |
| Starter pack: an Inertia request follows whatever redirect it's given; no client library (plan LG) | A redirect to another host or to `/oauth/*` answers an Inertia request as a full page visit (`HandleInertiaRequests`), so a sign-in at `id` returns to the product; `composer.json` autoloads `packages/legatus` (Legatus, the client library) for the suite; `POST /v1/report` takes CPU, memory and disk as optional | The products join Atrium through one package, proved here against Atrium (IA §9 step 7) | 9 Nov 2026 |

### Every file that differs from the release

`RecordedExceptionsTest` keeps this list equal to `php artisan core:status`, so a difference can't go unrecorded.

<!-- differs -->
- `app/Actions/People/AcceptInvitation.php`
- `app/Actions/People/InvitePerson.php`
- `app/Actions/People/MergePeople.php`
- `app/Actions/People/RemoveMembership.php`
- `app/Actions/People/SetMembershipActive.php`
- `app/Actions/People/SplitPerson.php`
- `app/Actions/Tenancy/UpdateWorkspace.php`
- `app/Console/Commands/AppAgentsCommand.php`
- `app/Exceptions/Problem.php`
- `app/Http/Auth/ApplyRememberRule.php`
- `app/Http/Auth/RefuseLockedAccount.php`
- `app/Http/Auth/SignInPages.php`
- `app/Http/Controllers/Auth/AccountController.php`
- `app/Http/Controllers/Auth/StepUpController.php`
- `app/Http/Controllers/BrandController.php`
- `app/Http/Controllers/File/FileController.php`
- `app/Http/Controllers/HomeController.php`
- `app/Http/Controllers/InvitationController.php`
- `app/Http/Controllers/Platform/LensController.php`
- `app/Http/Controllers/Setting/SettingExceptionController.php`
- `app/Http/Controllers/Setting/WorkspaceProfileController.php`
- `app/Http/Controllers/Setting/WorkspaceSettingController.php`
- `app/Http/Middleware/HandleInertiaRequests.php`
- `app/Http/Middleware/Idempotent.php`
- `app/Http/Middleware/ResolveTenant.php`
- `app/Http/Middleware/SecurityHeaders.php`
- `app/Lists/EmailLogList.php`
- `app/Models/Concerns/Audited.php`
- `app/Models/Concerns/HasPublicId.php`
- `app/Models/IdempotencyKey.php`
- `app/Models/Invitation.php`
- `app/Models/Membership.php`
- `app/Models/Person.php`
- `app/Models/Tenant.php`
- `app/Models/User.php`
- `app/Providers/AppServiceProvider.php`
- `app/Providers/FortifyServiceProvider.php`
- `app/Support/Access/Permissions.php`
- `app/Support/Access/StandardRoles.php`
- `app/Support/Auth/AuthEvents.php`
- `app/Support/Brand/Identity.php`
- `app/Support/Files/FileResponse.php`
- `app/Support/Files/ScanFile.php`
- `app/Support/Format.php`
- `app/Support/Imports/CheckImport.php`
- `app/Support/Imports/RunImport.php`
- `app/Support/InputFields/Catalogue.php`
- `app/Support/Logs/ExportAuditLog.php`
- `app/Support/Logs/LogPurger.php`
- `app/Support/Logs/PurgeLogs.php`
- `app/Support/Navigation/Nav.php`
- `app/Support/Notifications/EventCatalogue.php`
- `app/Support/Notifications/Events/AccountWords.php`
- `app/Support/Notifications/Events/ExportReady.php`
- `app/Support/Notifications/Events/ImportFinished.php`
- `app/Support/Notifications/Events/NotificationDigest.php`
- `app/Support/Notifications/Events/PasswordResetRequested.php`
- `app/Support/Notifications/Events/ReadinessFailed.php`
- `app/Support/Notifications/Events/WorkspaceOwnerAdded.php`
- `app/Support/Notifications/Events/WorkspaceStatusChanged.php`
- `app/Support/Notifications/NotificationEvent.php`
- `app/Support/Notifications/Notify.php`
- `app/Support/Notifications/Recipient.php`
- `app/Support/Notifications/RecipientResolver.php`
- `app/Support/Notifications/SendEmailMessage.php`
- `app/Support/Operations/Dashboard.php`
- `app/Support/Operations/Readiness.php`
- `app/Support/People/InviteImported.php`
- `app/Support/People/PersonMatcher.php`
- `app/Support/Privacy/EraseData.php`
- `app/Support/Privacy/MakeCopy.php`
- `app/Support/Privacy/Parts/ProfilePart.php`
- `app/Support/Privacy/Parts/SignInPart.php`
- `app/Support/Privacy/PersonalData.php`
- `app/Support/Search/Kinds/PageKind.php`
- `app/Support/Settings/Catalogue.php`
- `app/Support/Settings/Entry.php`
- `app/Support/Settings/Limiters.php`
- `bootstrap/app.php`
- `bootstrap/providers.php`
- `config/auth.php`
- `config/brand.php`
- `config/database.php`
- `config/fortify.php`
- `config/logging.php`
- `config/mail.php`
- `config/queue.php`
- `config/scramble.php`
- `config/services.php`
- `config/session.php`
- `config/setting.php`
- `deploy/launchd/queue.plist`
- `deploy/production.env.example`
- `lang/en/account.php`
- `lang/en/api.php`
- `lang/en/audit.php`
- `lang/en/auth.php`
- `lang/en/error.php`
- `lang/en/home.php`
- `lang/en/log.php`
- `lang/en/lookup.php`
- `lang/en/nav.php`
- `lang/en/notification.php`
- `lang/en/notification_template.php`
- `lang/en/operations.php`
- `lang/en/permission.php`
- `lang/en/privacy.php`
- `lang/en/role.php`
- `lang/en/setting.php`
- `resources/css/app.css`
- `resources/css/shell.css`
- `resources/js/app.ts`
- `resources/js/components/AppSidebar.vue`
- `resources/js/components/setting/WorkspaceFields.vue`
- `resources/js/components/shell/IdleWatch.vue`
- `resources/js/components/shell/StepUp.vue`
- `resources/js/layouts/AuthShell.vue`
- `resources/js/lib/dialog.ts`
- `resources/js/lib/format.ts`
- `resources/js/pages/Error.vue`
- `resources/js/pages/Home.vue`
- `resources/js/pages/auth/ConfirmPassword.vue`
- `resources/js/pages/auth/SignIn.vue`
- `resources/js/pages/log/Purge.vue`
- `resources/js/pages/platform/FailedJob.vue`
- `resources/js/pages/platform/FailedJobs.vue`
- `resources/js/pages/platform/Home.vue`
- `resources/js/pages/platform/LensCreate.vue`
- `resources/js/pages/platform/Operations.vue`
- `resources/js/pages/platform/Workspace.vue`
- `resources/js/pages/platform/WorkspaceCreate.vue`
- `resources/js/pages/setting/Workspace.vue`
- `resources/js/pages/setup/Wizard.vue`
- `resources/js/types/index.ts`
- `resources/js/types/vue-shims.d.ts`
- `resources/views/app.blade.php`
- `resources/views/components/brand-tokens.blade.php`
- `routes/auth.php`
- `routes/console.php`
- `routes/email.php`
- `routes/import.php`
- `routes/log.php`
- `routes/operations.php`
- `routes/person.php`
- `routes/platform.php`
- `routes/setting.php`
- `routes/web.php`
- `tests/Feature/Access/RoleTest.php`
- `tests/Feature/Api/ApiTest.php`
- `tests/Feature/Api/IdempotencyTest.php`
- `tests/Feature/Auth/PasswordChangeTest.php`
- `tests/Feature/Auth/RememberMeTest.php`
- `tests/Feature/Auth/SessionHardeningTest.php`
- `tests/Feature/Auth/TwoFactorTest.php`
- `tests/Feature/Brand/BrandLayerTest.php`
- `tests/Feature/Core/RenameTest.php`
- `tests/Feature/Files/FileRouteTest.php`
- `tests/Feature/Guards/ApiGuardTest.php`
- `tests/Feature/Guards/LogGuardTest.php`
- `tests/Feature/Guards/NoHardcodedListsTest.php`
- `tests/Feature/Guards/NoHardcodedMailTest.php`
- `tests/Feature/Guards/PermissionGateTest.php`
- `tests/Feature/Guards/PersonalDataGuardTest.php`
- `tests/Feature/Guards/TenantGuardTest.php`
- `tests/Feature/Guards/VaultGuardTest.php`
- `tests/Feature/Imports/ImportTest.php`
- `tests/Feature/InputFields/InputFieldPagesTest.php`
- `tests/Feature/InputFields/PersonIdentifierTest.php`
- `tests/Feature/Languages/RegionTest.php`
- `tests/Feature/Log/AuditLogTest.php`
- `tests/Feature/Log/ExportTest.php`
- `tests/Feature/Log/PurgeTest.php`
- `tests/Feature/Mail/AccountMailTest.php`
- `tests/Feature/Mail/EmailPagesTest.php`
- `tests/Feature/Mail/EngineTest.php`
- `tests/Feature/Notifications/BellTest.php`
- `tests/Feature/Notifications/SweepTest.php`
- `tests/Feature/Operations/OperationsTest.php`
- `tests/Feature/Operations/ReadinessTest.php`
- `tests/Feature/People/IdentityTest.php`
- `tests/Feature/People/PeoplePagesTest.php`
- `tests/Feature/People/PersonIsolationTest.php`
- `tests/Feature/People/PersonMatcherTest.php`
- `tests/Feature/People/PersonSplitTest.php`
- `tests/Feature/People/ResolveTenantTest.php`
- `tests/Feature/Search/SearchTest.php`
- `tests/Feature/Service/LensTest.php`
- `tests/Feature/Service/ServiceIdentityTest.php`
- `tests/Feature/Settings/SettingPagesTest.php`
- `tests/Feature/Setup/FreshInstallTest.php`
- `tests/Feature/Setup/SetupTest.php`
- `tests/Feature/Shell/ShellTest.php`
- `tests/Feature/Tenancy/CreateWorkspaceTest.php`
- `tests/Feature/Tenancy/TenancyTest.php`
- `tests/Feature/Tenancy/TenantSweepTest.php`
- `tests/Pest.php`
- `tests/Support/TenantSweep.php`
- `tests/Support/normal-form.php`
- `tests/TestCase.php`
<!-- /differs -->


## Nuntius

The send path is built (plan NS, `reji/docs/reference/notify.md`), and `notify` is on by default in `config/modules.php`. A provider is connected on the console, under Messages → Channels, and its key can't be read back. Sender names are recorded with `notify:sender`, and a server's own wording is allowed with `notify:finished-text`. `notify:check` runs every 10 minutes. No real provider has been called yet.

## Legatus

The client library products install (plan LG): `packages/legatus`, its README the install guide, `reji/docs/reference/legatus.md` its
reference. Proved in `tests/Feature/Legatus/` against Atrium itself. Every change a product needs to install it is in
`reji/tasks/other-apps.md`, done from that product's own session after Atrium is deployed. Locally a throwaway product used port 8103
for the end-to-end check (9 November 2026).

## The repository

`origin` is `git@github-tu:tutelora/atrium.git`, private. The SSH alias signs in as `tutelora`; `gh` here is signed in as another account that can't see it, so use git over the alias. Every commit is by `tutelora`, with no attribution trailer, and no AI tool is named in commits, docs or code. Only `develop` is pushed, and a push deploys nothing: staging on cPanel is deployed by hand.

## Words

On the console, an organisation is a **client**. In code, and for people at `id`, it stays an organisation. The software that calls Atrium with a key is a **server**.

## Running it

- **Hosts here:** people at `http://id.localhost:8100`, the console and API at `http://api.localhost:8100`, short links at `http://ink.localhost:8100` (browsers send `*.localhost` to this computer). Plain `localhost:8100` answers 404, on purpose.
- **The local console:** sign in at `http://api.localhost:8100/login` as the local Service, whose details are in `.env`, which isn't in the repository.
- **Port:** block 8100–8109 (`/Users/reji/dev/registry.html`). The local launch settings, which aren't in the repository,
  serves the app on 8100, the mockups on 8101 and the docs on 8102.
- **Dev agents:** `com.tutelora.atrium-8100`, `-queue`, `-queue-bulk` and `-schedule`, running since 7 November 2026 and restarted by launchd, so `http://id.localhost:8100` stays up. Restart one with `launchctl kickstart -k gui/501/<agent>`. After a change to a job, `php artisan queue:restart`. Logs in `~/Library/Logs/tutelora/`.
- **Database:** `atriumdb` (development), `atrium_test` (the suite).
- **Queue:** Redis on `127.0.0.1:6379` (`QUEUE_CONNECTION=redis`). Its tests skip themselves if Redis isn't running.
- **Tests:** `npm run build` first, or every page test fails with "Not a valid Inertia response"; then
  `php artisan test --parallel`.

## Hosts

Nothing answers on these yet. One app, one deployment. Accounts, sign-in, organisations, subscriptions and billing are all at
`id.tutelora.com`, and the module APIs and staff console at `api.tutelora.com` (the Architect's decisions, 6 November 2026, replacing the spec's
`accounts.tutelora.app` and `api.tutelora.app`), with `tutel.ink` for the redirect and nothing else. The first deployment is the cPanel account on
these real names, `api.tutelora.com` holding the app and `id.tutelora.com` on the same folder, running as staging until the VPS (the Architect,
9 November 2026, replacing `*.tutelora.dev`; step by step in `reji/docs/reference/deployment.html`). See `reji/plans/2026-11-06-IA-identity-architecture.md`, whose §6 sets the rules that protect sign-in on the
shared host.

## Commits

The author is `tutelora` (this repository's own git config), and a commit message carries no attribution trailer. **There is no
remote yet,** so nothing deploys; this section says what a push deploys once there is one.

## Plans

Plans and tasks live in `reji/plans` and `reji/tasks`, named by date and subject.
