Accounts¶
Organizations and the users inside them — the tenant and identity layer every other domain reads from.
Accounts answers two questions for the whole product: who is this customer, and are
they allowed to do this right now? Every org that signs up for Beacon gets an
Organization record — the top-level tenant — and the
people who log in are users that belong to it. When Billing decides what
a customer is entitled to, or Messaging decides whether a send is
allowed, both start from a fact that lives here: an org's identity, its status, and
how many seats it has.
That makes Accounts the quiet foundation. It does the least glamorous work in Beacon — it sends nothing, charges nothing — but if it's wrong, every other domain is wrong with it. A suspended org that still sends, or a seat count that drifts from reality, are Accounts failures that surface as Billing or Messaging bugs.
Capabilities¶
What this domain lets you do, in plain terms. Each links to where it's documented in depth.
- Manage an organization — its profile (name), seat allotment, and
status. The org is the unit everything else hangs off; see Organization. - Suspend / reinstate an organization — cut off a customer's ability to send (for non-payment or abuse) and restore it later, without deleting anything. See Suspend an organization.
- Be the seat-count authority — track how many paid user slots an org has and, together with seat assignments, how many are actually in use. Billing reads this to enforce plan limits; see the Seat glossary entry.
- Answer "who and what status?" for other services — expose org identity and
statusover a small HTTP read API (GET /orgs/{id}) that billing-service and messaging-service call before they act.
Users are real, but lightly documented today
accounts-service owns users as well as organizations, and an org has many users
(a DB foreign key, see Organization). The
User model isn't written up yet — the owning service is still a stub. Treat "users
belong to an org, and an assigned user consumes a seat" as the canonical shape until
the model page lands.
How it fits together¶
One service owns this domain end to end. accounts-service (Java/Spring on MySQL) is the sole writer of org and user data; everyone else is a reader.
flowchart LR
web[Web UI / internal console] -->|create, suspend, edit| acct[accounts-service]
acct --> db[(MySQL: accounts.organizations)]
billing[billing-service] -->|GET /orgs/{id}| acct
messaging[messaging-service] -->|GET /orgs/{id} before each send| acct
billing -.->|syncs seat_limit on plan change| acct
The shape to remember:
- accounts-service writes; everyone else reads over the API. Org data has exactly one
source of truth. Other services fetch
GET /orgs/{id}(which returns seat count andstatus) when they need it, and must not cache it as long-term truth — a cached-stalestatusis how a suspended org slips a message out. - Billing reaches back in. When a plan changes, billing-service
pushes the new seat allowance to Accounts — it sets the org's
seat_limitso the two stores agree on how many seats the org is entitled to. This is the one write to org data that doesn't originate inside the domain, and it's a deliberate, narrow exception. - Messaging only reads, and only to gate. Before every send, messaging-service checks
the org's
statusviaGET /orgs/{id}and rejects when it'ssuspended. Accounts has no idea messages exist; it just answers the question.
The models in this domain¶
| Model | What it is | Owner | Store |
|---|---|---|---|
| Organization | The top-level tenant — name, status, seat_limit |
accounts-service | MySQL accounts.organizations |
| User (stub) | A person who logs in and belongs to one org; an assigned user consumes a seat | accounts-service | MySQL |
The Subscription and Plan that an org
pays under live in Billing, not here — Accounts holds the tenant, Billing
holds what the tenant bought. They're linked only logically: one org has one active
subscription, joined by org_id across two separate MySQL databases with no foreign
key between them (a cross-store relationship).
Seats: the number two domains have to agree on¶
Seats are where Accounts and Billing meet, so it's worth being precise about who owns what. A seat is one paid user slot. Two numbers track it, and they live on different sides of the boundary:
seat_limit(on the Organization, in Accounts) — how many users the org is allowed to have. This is set by Billing when the plan changes and synced into the org record, so Accounts can enforce it locally when assigning users.seats/seats_in_use(on the Subscription, in Billing) — how many seats the plan grants, and how many are actually assigned.seats_in_useis view-derived (computed byactive_subscriptions_vfrom accounts seat assignments), not a stored column.
The same real-world quantity — "how many people can this org have, and how many does it
actually have?" — is therefore reflected on both sides by design. Keep the ownership straight: Accounts owns
the assignment of users to seats; Billing owns the entitlement (how many seats the plan
includes) and the rule that seats >= seats_in_use, which is checked in app code, not in
the database. When those drift, look first at the seat_limit sync from Billing.
Status and the suspend lifecycle¶
An org's status is a small, DB-enforced enum — active or suspended, defaulting to
active. That single field is the lever the whole product reads to decide whether a
customer can operate.
stateDiagram-v2
[*] --> active: org created
active --> suspended: admin suspends (non-payment / abuse)
suspended --> active: admin reinstates
A suspended org isn't deleted or locked out of the app — users can still sign in. What
changes is that sends are blocked: messaging-service checks status before each send
and rejects when it's suspended. Reinstating flips it back to active and nothing is
lost. Walk the full path, including the admin console and the cross-service check, in
Suspend an organization.
Suspension is an Accounts decision, enforced in Messaging
Note the split: the state (status = suspended) lives in Accounts and is set
through accounts-service. The enforcement (refusing the send) happens in Messaging,
which reads that state per send. Accounts doesn't reach into Messaging to stop
anything — it just tells the truth when asked. This is the domain's whole philosophy in
miniature: own the fact, let others act on it.
Notes & nuances¶
Boundaries — what's Accounts, what isn't
In Accounts: organizations, users, seat assignment, org status, and the read API
that exposes identity/status/seat-count to other services.
Not in Accounts: what an org pays for — plans, subscriptions, invoices,
proration, and the seats entitlement — all belong to Billing. Anything
about sending — templates, channels, schedules, delivery — belongs to
Messaging. The line is clean: Accounts is who exists and may they
operate; Billing is what they bought; Messaging is what they sent.
Read-only for everyone but accounts-service
The single-writer rule is the most important invariant here. If you find another
service writing org data directly (rather than calling accounts-service, or going
through Billing's narrow seat_limit sync), that's a bug to flag — it breaks the
"one source of truth" guarantee the rest of the docs rely on.
The stack, stated once
accounts-service is Java/Spring on MySQL — and every source agrees: the service's
language: java frontmatter, its prose, the services index
diagram, and the org chart all line up. Worth stating because the two MySQL-backed
services run different languages — billing-service is
Ruby/Rails — so they share one engine but not a toolchain.
Why this domain is only ~75% confident
accounts-service is still a stub (its own page is
~20% confident) and the User model isn't documented yet. The org-level facts on this
page are solid — they're verified against Organization and
the suspend flow — but the full HTTP surface and
the user/seat-assignment mechanics need confirmation against the source repo before
this domain can claim more.
Related pages¶
In this domain: accounts-service, Organization, Suspend an organization
Confidence: 75/100 · Last reviewed: 2026-06-14 · Status: active