# Core concepts

The whole object model in one page. Everything below is scoped to a project;
there is no global anything.

## Organizations and projects

An **organization** — a workspace — is the billing and membership boundary:
your team. It owns **projects**, along with the sending credentials, API keys
and people that are not per-project. See
[Workspaces & roles](/docs/workspaces).

A **project** is one app. It carries its own sender identity (the verified
from-address), branding, brand voice, audience, integrations and revenue view.
Two projects never share contacts, lists, templates or send history.

Every project has a **slug** — `acme-app` — and slugs are **globally unique**,
not merely unique within an organization. That is what lets a URL, an API
identifier, an object-storage prefix and an unsubscribe link all agree on one
name. Creating a project whose slug is taken returns `409`.

Wherever the API takes a `projectId`, it accepts the slug or the internal
`prj_…` id. The slug is the friendlier choice and the one the dashboard URLs
use: `/<team>/<project>/<area>`.

> Renaming a project's slug cascades through the database, but unsubscribe
> links already embedded in delivered email point at the old one. Treat a slug
> as permanent once you have sent from it.

## Contacts, lists and subscriptions

A **contact** is a person, unique by email within a project. It carries a
name, language, free-form tags, and the `source` that introduced it.

A **list** is an audience within the project — `newsletter`,
`product-updates`. Every project has a default list.

A contact's relationship to a list is a **subscription**, and that is where
the consent state lives: `subscribed`, `unsubscribed` or `pending`, with the
timestamp and the reason it changed. Unsubscribing is per list, not per
person, and it records *how* it happened — a preference-centre form, a
one-click header, a hard bounce, a spam complaint, or an operator.

Two of those reasons are **suppressions**: a hard bounce or a complaint. They
are protected. A contact sync cannot resubscribe a suppressed address, no
matter what the incoming payload claims — the response tells you how many it
refused instead. This is the rule that keeps a well-meaning nightly reconcile
job from destroying your sender reputation. See
[Unsubscribes & suppression](/docs/compliance).

## Templates and campaigns

An **email template** is an HTML row in the project's template library, seeded
from React Email starters with the project's branding **baked in at seed
time**. Branding is not a runtime token: a template is already yours by the
time you render it.

Rendering interpolates a small set of runtime tokens — `{{contact.email}}`,
`{{unsubscribeUrl}}` and friends — through one code path, so what preview
shows is what goes on the wire.

A **campaign** is one broadcast: a subject, a template, a recipient rule, and
a status that walks `draft → scheduled → sending → sent`. Campaigns support
per-language variants (English and Portuguese subject/body today), so one
campaign serves a bilingual audience without duplicating it.

## Sends

Every delivered email — from a campaign, an automation, or a transactional
event — writes one row to the **send log**, tagged with its `source`. The row
captures the subject and template at send time so history stays readable even
if the campaign is later edited or deleted.

Delivery, bounce and complaint notifications arrive from the sending provider
by webhook and update the same row, so a send moves `queued → sent →
delivered` and can end at `bounced` or `complained`. Opens and clicks land here
too.

That makes the send log the one place to answer "did this actually arrive" —
`GET /api/sends`, or the Sends view in the dashboard.

## Automations

An **automation** is a multi-step drip sequence: a trigger, then ordered steps
with delays between them. A contact entering the trigger starts a **run**,
which advances step by step on a schedule.

Triggers are tag-based, which means your app lights them by tagging a contact
— `POST /api/contacts/event` — rather than by knowing anything about the
sequence. Steps can skip themselves if a tag is present, which is how a
payment nudge stops as soon as the payment lands.

See [Automations](/docs/pharos-automations) for the step shape and worked
examples.

## Transactional events

A **transactional event** is a single triggered email: a receipt, a result, a
contact-form notification. You submit an event type, an idempotency key, a
recipient and a payload; Pharos owns composition, suppression, delivery and
recording.

Two things distinguish it from a campaign. First, it is idempotent — the
`(project, idempotencyKey)` pair is unique, so a retry is free. Second, it has
a **preview mode** that composes the email and returns it without sending
anything, gated by a different scope than delivery.

`notificationCategory` decides how the mail is treated: `product` mail honours
subscription state, gets an unsubscribe header and is queued with retries;
`operational` mail goes to your own team inbox, skips contact handling
entirely, and is delivered synchronously so a human waiting on it learns
immediately if it failed.

## Beacons, revenue and reviews

Three read-oriented surfaces share one `insights:read` scope, because they
answer the same question — how is the product doing — and none of them can
change anything.

- **Beacons** watch Reddit, Hacker News and Bluesky for mentions of terms you
  register, and collect hits for review.
- **Revenue** syncs store and payment metrics into a per-project summary.
- **Reviews** pulls app-store reviews so a bad one can be answered.

## Credentials

Three credentials reach a project, and they differ in *reach*, not in power:

| Credential | Prefix | Reaches |
| --- | --- | --- |
| Dashboard session | — | Every project in every org you belong to |
| Project API key | `phk_…` | Exactly one project |
| Organization API key | `pha_…` | Any project in its organization |

What a key may *do* is its **scopes**, named `resource:action`. The two splits
that matter are `campaigns:preview` vs `campaigns:send` and `events:write` vs
`events:send` — see [Authentication & keys](/docs/authentication).
