# Core concepts

How to think about Platform Lydian before you touch an endpoint. This page is
about the mental model; the [Quickstart](/docs/quickstart.html) gets you to a
first API call, and the [API reference](/docs/) documents every endpoint.

---

## The big picture

Platform Lydian is a multi-tenant administrative backbone. It gives your
application five kinds of records — people, events, documents, invoices, and
communications — behind one API, with a twist that makes it different from
ordinary REST CRUD: **the platform does not ship a fixed schema.** You define
your own attributes, compose them into types, and the platform validates every
record against the type it references.

```
                            ┌─────────────────────────────┐
                            │           Tenant            │
                            │  (hard isolation boundary)  │
                            └──────────────┬──────────────┘
                                           │
              ┌──────────────┬─────────────┼─────────────┬──────────────┐
              ▼              ▼             ▼             ▼              ▼
          People         Events        Documents      Invoices    Communications
        (who exists)  (what happens) (what is kept) (what is owed) (what is said)
              │              │             │             │              │
              └──────────────┴─────────────┴─────────────┴──────────────┘
                                           │
                              all validated against
                                           ▼
                      Types  ←──── composed from ────  Attributes
              (Student, Lesson, …)                (Instrument, Fee, …)
```

Two ideas do most of the work:

1. **Everything lives inside a tenant.** One tenant, one namespace, one API key.
2. **Every record kind is typed by a tenant-defined schema** built from a shared
   pool of attributes.

Understand those two and the rest of the platform is details.

---

## Tenancy and isolation

A **tenant** is the top-level namespace. Every other record — every person,
event, document, invoice, message, attribute, and type — belongs to exactly one
tenant, and every API path says which one:

```
/tenants/{tenantId}/people
/tenants/{tenantId}/events
/tenants/{tenantId}/invoices/{invoiceId}/payments
```

Isolation is hard, not conventional. Your API key is issued for one tenant and
reaches only that tenant's paths; requests to any other tenant's paths return
`403`, and record ids from another tenant are simply `404` under yours —
existence is never leaked across the boundary. There are no cross-tenant
queries, references, or leaks to design around.

The practical consequence: **from inside your app the API is effectively
single-tenant.** You receive a tenant id and a key from the platform operator,
put both in server-side configuration, and never think about other tenants
again.

---

## The composable data model

This is the unconventional part, and the one concept genuinely worth learning
before writing code. Three layers:

```
Attribute      "Instrument"  (string, required, maxLength 40)   ─┐
Attribute      "Skill Level" (picklist of three options)         ├─ one reusable pool
Attribute      "Session Fee" (number, min 0)                    ─┘  per tenant
                        │
                        │  composed into (by id)
                        ▼
Type           PeopleType "Student"  = base fields + [Instrument, Skill Level]
               EventType  "Lesson"   = base fields + [Session Fee]
                        │
                        │  instantiated as
                        ▼
Record         Person  { firstName, lastName, email,
                         attributeValues: { <instrument-id>: "cello", … } }
```

### Attributes

An **attribute** is a named, described, typed value definition — "Instrument is
a required string of at most 40 characters". There are eight attribute kinds:
`string`, `number`, `boolean`, `date`, `email`, `address`, `picklist`, and
`person`. The `person` kind holds references to person records and is how you
build typed relationships — a "Lesson" event type with `student` and `teacher`
person attributes, for example.

Attributes form **a single shared pool per tenant**. The same "Session Fee"
attribute can appear on an event type and an invoice type; define it once,
reference it everywhere.

### Types

A **type** composes attributes (by id, in order) into a schema for one of the
five record kinds: `PeopleType`, `EventType`, `DocumentType`, `InvoiceType`,
`CommunicationType`. A record that references a type must satisfy it — required
attributes must have values, values must match their attribute's constraints,
and values for attributes the type does not declare are rejected.

Every kind also has **base fields** — structural fields that exist on every
record regardless of type. A person always has `firstName`, `lastName`,
`email`; an event always has `name` and `startAt`. Base fields are not
attributes and should not be re-modeled as attributes. Every type response
carries a read-only `baseFields` array describing them, so a client (or a form
builder) can discover the full effective schema machine-readably.

Every tenant comes pre-seeded with a **"Base" type** for each of the five kinds
(marked `"base": true` in responses). Records created without an explicit type
id get the Base type — just the base fields, no extra attributes. That means
you can build a working app without defining a single attribute, and adopt the
type system when your domain demands it.

### Records

A **record** (person, event, document, invoice, or communication) names its
type and carries its extra values in `attributeValues` — a map keyed by
**attribute id**, not attribute name:

```json
{
  "firstName": "Ada",
  "lastName": "Lovelace",
  "email": "ada@example.com",
  "peopleTypeId": "3f7c…",
  "attributeValues": { "0198a4f2-…": "cello" }
}
```

Keying by name instead of id is the single most common integration mistake.

### What "versioned" means

Attributes and types carry a `major.minor` version, and every record snapshots
the version of its type that was current when the record was created (in
`peopleTypeVersion`, `eventTypeVersion`, and so on). The intent: minor versions
are backwards-compatible changes, major versions are breaking changes requiring
data migration, and the per-record stamp identifies which records were written
against which schema generation.

Be aware of the current honest state: **new attributes and types always start
at version `1.0`, and no API operation bumps a version yet.** You can update a
type in place (rename it, add or remove attributes), and existing records keep
their original stamp — but today that stamp will always read `1.0`. The
version machinery is a forward-compatibility contract, not something you
interact with yet.

---

## People

A **person** is any human your app knows about — student, parent, teacher,
client. People matter more than the other record kinds because everything else
points at them:

- events name people as **participants**,
- invoices name a person as the **recipient**,
- messages name people as **sender and recipients**,
- document permissions are granted to people,
- person-kind attributes let any record reference people.

Two consequences of being the hub. First, **create people early** — several
parts of the platform (documents, communications) are unusable until at least
one person exists. Second, **references block deletion**: a person referenced
by an event, invoice, message, or attribute value cannot be deleted until the
references are unwound. Nothing ever cascades.

### The acting person

Documents and communications are person-scoped: the platform needs to know
*who* is checking out a file or reading an inbox — for permissions, audit
attribution, and filtering. Those endpoints take a second header,
`X-Acting-Person`, carrying a person id from your tenant. It identifies, but
does not authenticate: anyone holding the API key can act as anyone. The API
key is the security boundary; the acting person is workflow attribution.

---

## Documents

A **document** is versioned content plus typed metadata. The content — native
markdown or an uploaded binary file — lives in object storage as a chain of
**immutable versions**; the document record carries the title, description,
attribute values, and a pointer to the current version.

Content changes use a **check-out / check-in lock**: check-out takes an
exclusive lock, check-in writes version N+1 and releases it. Old versions stay
readable forever. Metadata edits don't need the lock.

Access is controlled by **roles**: a role is a tenant-defined set of
permissions (`READ`, `DOWNLOAD`, `CHECKOUT`, `CHECKIN`, `DELETE`,
`MANAGE_ROLES`, …),
assigned to a person either tenant-wide or on a single document; effective
permissions are the union. Two roles are seeded per tenant — **Admin**
(everything) and **Reader** (read + download) — and a document's creator
automatically gets Admin on it.

Every state-changing action lands in an append-only **audit trail** (reads are
not audited; downloads are), and document content is **full-text indexed** —
`GET /documents/search?q=` returns permission-filtered, relevance-scored hits.

---

## Events and scheduling

An **event** is anything with a time: a meeting, a lesson, a deadline, a to-do
(a to-do's due date is its `startAt`). Events have a name, an optional
description, a start instant, an optional end, and — like every record kind —
a type with extra attributes.

Events carry **participants** in the style of a calendar invite: each
participant is a person with a role (`ORGANIZER`, `ATTENDEE`, `OPTIONAL`) and
an RSVP status (`PENDING`, `ACCEPTED`, `DECLINED`, `TENTATIVE`).

The event list is **queryable by time range** — `GET /events?from=…&to=…`
windows on `startAt` (from inclusive, to exclusive) — which is the primitive
you build calendars, agendas, and billing periods on.

---

## Billing

Billing is **record-keeping, not payment processing**: invoices track what is
owed, payments record money that arrived outside the platform (cash, check,
transfer). No money moves through the API.

An **invoice** bills one person and carries line items (`description`,
`quantity`, `unitPrice`; the amount is always derived, never stored). Its
lifecycle is strict:

```
DRAFT ──issue──► ISSUED ──payments cover total──► PAID
  │                 │                              │
  └──void──►  VOID  ◄──void (only if no payments)──┘        PAID ──payment deleted──► ISSUED
```

Content is editable only while `DRAFT`. `PAID` is derived — recomputed whenever
a payment is recorded or deleted, never set by a client. Invoice numbers
(`INV-000042`) come from an atomic per-tenant sequence at creation; deleted
drafts leave gaps, which is expected.

The billing–scheduling bridge is **line items generated from events**: point a
draft invoice at a number attribute your events carry (say "Session Fee"), and
the platform turns matching events into line items, each stamped with its
`sourceEventId`. That stamp drives a double-billing guard — an event billed on
a non-void invoice is skipped by later generations and cannot be deleted.

---

## Communications

A **communication** is an in-platform message; nothing is sent by email or SMS.
One record kind, two channels:

- **`MESSAGE`** — email-like, person-to-person. Starts as a `DRAFT`, is
  editable until sent, then becomes immutable. Sent messages are permanent —
  there is no delete, like real email.
- **`NOTIFICATION`** — a brief one-shot announcement, born `SENT`.
  Participants may dismiss (delete) notifications.

Replies form **threads**: a reply copies its parent's `threadId`, so a thread
is every record sharing one `threadId`, ordered by send time. Each recipient
carries their own **read state**, and the **inbox endpoints**
(`/inbox`, `/sent`, `/drafts`, `/unread-count`) present a unified,
per-person view — which is why communications require the `X-Acting-Person`
header.

**The platform also talks.** Three domain triggers auto-emit system
notifications — an invoice being issued, a document being checked in, a person
being added as an event participant. They arrive with `"source": "SYSTEM"` and
no sender. Your inbox will contain records you never created; filter on
`source` if you only want human messages.

---

## How the pieces connect

The five kinds are not silos. The causal links, explicitly:

```
                         ┌───────────┐
        participants ──► │  Events   │ ── "Session Fee" attribute ──┐
             │           └───────────┘                              │ from-events
             │                 │ participant added                  ▼
       ┌───────────┐           │                              ┌───────────┐
       │  People   │           ▼                              │ Invoices  │
       └───────────┘   system notification                    └───────────┘
         ▲   ▲                 │                                    │ issued
         │   │                 ▼                                    ▼
  recipient  acting     ┌────────────────┐  ◄── system notification ┘
         │   person     │ Communications │
         │   │          └────────────────┘
       ┌───────────┐           ▲
       │ Documents │ ── checked in: system notification
       └───────────┘      (also: documents attach to messages)
```

- **Events → invoices.** Events carrying a number attribute become invoice line
  items via from-events generation; `sourceEventId` links each line back and
  guards against double billing.
- **Platform activity → notifications.** Issuing an invoice, checking in a
  document, and adding an event participant each notify the affected people
  automatically.
- **People ← everything.** Participants, recipients, invoice recipients,
  permission grantees, acting persons, and person-attribute values all point at
  person records — and every one of those references blocks deleting the
  person.
- **Documents ↔ communications.** Messages can attach documents by id
  (`attachmentDocumentIds`).
- **Attributes ← every type.** One attribute pool feeds all five type kinds, so
  a concept like "Session Fee" is defined once and means the same thing on an
  event and an invoice.

---

## Where to go next

- **[Quickstart](/docs/quickstart.html)** — zero to a created-and-read-back
  record in about ten minutes.
- **[Authentication & conventions](/docs/conventions.html)** — keys, the error
  envelope, timestamps, and the platform's current limits.
- **How-to guides** — [model your domain](/docs/model-your-domain.html),
  [manage a document](/docs/manage-a-document.html),
  [invoice from events](/docs/invoice-from-events.html),
  [work with the inbox](/docs/work-with-the-inbox.html).
- **[API reference](/docs/)** — every endpoint, request, and response shape.
