Pharos
Getting started.md

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.

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 slugacme-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.

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 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.