> For the complete documentation index, see [llms.txt](https://docs.layeronecloud.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.layeronecloud.com/ai/invariants.md).

# Platform invariants

The load-bearing rules that hold across the whole platform, with the page that explains each one.

These are the rules most likely to be broken by a change that looks correct locally. Each one links to the page that explains why it exists.

{% hint style="warning" %}
This page is a summary for orientation. It is mirrored in each repository's `AGENTS.md` so an agent working offline still has it. When the two disagree, the linked reference page is authoritative.
{% endhint %}

## Identity <a href="#identity" id="identity"></a>

* **The user is email-keyed.** Sign-in is Continue — OIDC for that domain, otherwise password. Do not reintroduce `paymenter_user_id` or any unused external-id column. See [Users and sign-in](/platform/identity/users-and-sign-in.md).
* **Passwordless `User` rows are placeholders** created by `get_or_create_guest_ticket_user`. Registration claims them. `_is_claimable_placeholder_account` stays unprivileged-only.
* **Console auth is `access_level == Admin`** via `@admin_required`, never `is_staff`. Client views check ownership (`_request_can_access_*`) and return 403. See [Staff roles](/platform/identity/staff-roles.md).
* **Staff roles are a default-deny allowlist** in `apps/accounts/staff_access.py`. A new console area is hidden from Support until it is listed. Blank `staff_role` means Support. Roles change only through `apply_console_role`.
* **Admin and Client are separate views, not a filter, and the mode is derived from the route — never chosen.** Nothing outside `AccessModeRouteMiddleware` may write the mode. A cross-host write is a 403, never a 302. Authenticated HTML is `no-store`. See [Access modes](/platform/identity/access-modes.md).
* Sign-in failure is always the generic "email or password", including for disabled accounts. Passkeys are 2FA after a password, never passwordless. MFA checks `confirmed_at`, not `MfaDevice.exists()`. See [Two-factor](/platform/identity/two-factor.md).

## Money <a href="#money" id="money"></a>

* **Paid-invoice credit goes through `record_account_credit_for_paid_invoice` only.** The ledger is append-only (`adjust_account_credit`). Feeds use `BillingEvent.log_visible()`. See [Ledger and metering](/platform/billing/ledger-and-metering.md).
* A `Subscription` must stay linked to its service. **Repricing a plan means a new `Plan` row**, never an edit of a live one. See [Billing model and catalog](/platform/billing/model-and-catalog.md).
* **Webhooks verify the signature, are idempotent on `(provider, event_id)`, and record failures&#x20;*****outside*** **`transaction.atomic()`** — a rollback otherwise discards the failure record along with the partial work.
* **PayPal is account credit only** (one-time and customer-approved monthly). Do not enable Reference Transactions or saved-agreement charges. See [Payments](/platform/billing/payments.md).

## Provisioning and the API <a href="#provisioning-and-the-api" id="provisioning-and-the-api"></a>

* Provisioning is dry-run locally (`BILLING_PROVISIONING_LIVE=False`). See [Provisioning](/platform/vps/provisioning.md).
* **API auth is the key header only** — never `request.user` or the session, which is what makes `@csrf_exempt` safe there. `/api/v1/` is legacy, `/api/v2/` is current. Locked until hourly billing is unlocked on an active account. See [Client API](/platform/client-api.md).

## Secrets and output <a href="#secrets-and-output" id="secrets-and-output"></a>

* **Secrets are write-only in forms.** A console secret field is a `MaskedSecretField` on a `MaskedSecretFieldsMixin` form (`apps/core/form_utils.py`): a stored value renders as `SECRET_MASK`, and an asterisks-only submission arrives blank, so "blank keeps what is stored" and the required-checks both stay correct.
* Recoverable secrets go through `apps/core/crypto.py` (`billing.crypto` is an alias): VM passwords via `set_root_password()`, ticket secrets via `add_ticket_secret`. Set `FIELD_ENCRYPTION_KEY` in production.
* **Never `|safe` user content.** Only admin-authored CMS `body_html` and `extra_head_html`.
* Do not record `/console/`. Session recordings are super-admin, default off, Support denied. The activity log carries no query strings and no credential paths. See [Observability](/platform/observability.md).

## Database <a href="#database" id="database"></a>

* **Production is PostgreSQL; local tests are SQLite.** Raw SQL, `extra()`, `RawSQL` and lock queries must be valid on PostgreSQL.
* `select_for_update()` together with a LEFT OUTER JOIN — a nullable `select_related`, or a filter across a nullable FK — needs `of=("self",)`. PostgreSQL raises `FOR UPDATE cannot be applied to the nullable side of an outer join`; **SQLite will not fail that**, so the test suite cannot catch it.
* A `TextChoices` change needs `makemigrations`.

## Front end <a href="#front-end" id="front-end"></a>

* Tokens live in `static/css/tokens.css`, components in `static/css/components.css`. Status colour comes from `l1_ui` filters, never a per-page `{% if %}`. There is no late CSS override layer.
* Tailwind utilities are `tw:`-prefixed — `tw:hover:bg-brand-strong`, never `hover:tw:…`. Rebuild with `sh scripts/build_css.sh`.
* Dark mode is console-only (`data-l1-theme-dark-supported`).
* Canonical URLs hardcode `https://layeronecloud.com`; do not make them request-relative.
* Support-visible pages may only link Support-readable routes, or wrap the link in `console_nav_is_support_staff`. Guard nullable FKs in `{% url %}`.
* `current_site` is on every template. Keep `base.html` and `public_sidebar.html` navigation in sync. See [Design system](/platform/design-system.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.layeronecloud.com/ai/invariants.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
