# Data model

For an outside developer, the Fayz data model collapses into a single idea: **you bring your own Supabase (BYOS)** and your project gets the **same schema shape** the platform runs at scale. That shape has three layers — the **core library**, the **plugin tables** (`plg_`) and **your app's tables** — plus one rule that makes it all fit together: **`tenant_id` + RLS is the isolation boundary.** Get that right and the classic question — "where does this table live?" — goes away.

```
public.*      base tables every business needs (tenants, people, tenant_members, role_permissions…)
plg_*         tables a plugin installs and owns (plg_shop_products, plg_courses_*…)
your tables   what your app/incubator adds on top (same rules: tenant_id + RLS)
```

{% callout type="info" %}
**Architecture in transition.** These docs describe the **target shape** — the `core` library and plugin tables with the `plg_` prefix — that is shipping with the *industry pools* wave. Today the base tables don't live in a separate `core.` schema: they all live in `public.*` (migration 001 is explicit — *all core entities live directly in the public schema*), and the `plg_` prefix is a convention already adopted by some modules, not a rule everyone follows yet. The command and the flow are already these; the labels and naming migrate along with the wave.
{% /callout %}

## The core library — the base tables

The **core** library (formerly called `saas_core`) brings what **every** business needs. It isn't a separate schema with its own rules — the tables live in `public.*`, ready and isolated:

| Core table | What it holds |
| --- | --- |
| `tenants` | your SaaS's customers (a salon, a clinic) — the root of isolation |
| `people` | the people in the business: customers, contacts, leads |
| `tenant_members` | who works in the tenant (professionals, staff) |
| `role_permissions` | roles and permissions inside the tenant |
| `appointments` | bookings, reservations, meetings |
| `transactions` | financial movements: money in and out |

**Every table carries `tenant_id` and has RLS.** That pair — column + policy — is what makes one tenant's row physically invisible to another. Isolation does **not** come from separating schemas; it comes from RLS keyed on `tenant_id`. That's the point the rest of the model rests on, and the details are in [RLS and multi-tenancy](/en/docs/data/rls).

## Plugin tables — the `plg_` prefix means ownership

A plugin installs **its own tables** with the **`plg_`** prefix (for example `plg_shop_products`, `plg_courses_*`). The prefix signals ownership: if a table starts with `plg_`, it belongs to a plugin — not to core, not to your app. It's a **convention** — adopted today by shop and courses and **recommended for new plugins** — not a rule every module already follows (some connectors create tables without the prefix).

See the `plg_` tables each plugin installs — along with their key columns — on the plugin's specs page in the [catalog](/en/docs/plugins).

Beyond the tables it creates, a plugin **reuses** the core tables it needs instead of copying them. If the scheduling plugin needs to know "who booked", it references `public.people`; it never stores a copy of the person. One source of truth per entity.

Each plugin ships its own migrations, and they follow rules that make `db apply` reproducible:

- **ordered** — the apply sequence is deterministic.
- **versioned** — every migration has a version; the runner knows what already ran.
- **fix-forward** — migrations are append-only: never edit a migration that has already been applied, write the next one that fixes it. There is no `.down.sql`.
- **idempotent** — running again neither breaks nor duplicates.

## Your app's tables

When your product needs fields or tables that neither core nor a plugin provides, your app (or an [incubator plugin](/en/docs/building-plugins/incubator)) creates its own tables. That's where the specific richness of your domain lives. The rule doesn't change: **they must carry `tenant_id` and have RLS**, exactly like core and plugins. Your tables aren't an exception to isolation — they're just like every other table.

## The golden rule: same shape, three tiers

The same schema shape — core + `plg_` + your tables, all isolated by `tenant_id` + RLS — holds wherever the app runs. That's what makes your BYOS identical to what the platform operates at scale.

{% callout type="info" %}
**How the platform hosts at scale (context — your BYOS is the same shape).** The platform serves three tiers with the **same shape**: ① an **industry pool** — one Supabase project shared by many tenants in the same industry, isolated only by `tenant_id` + RLS; ② **custom objects** — extra tables in the same pool via a generic mechanism, no DDL (no stable external contract yet); ③ **dedicated database** — a project of your own, same shape, where promoting means copying the data and swapping the connection string. You don't need any of this to build: your BYOS is born in the same shape, and moving between tiers never rewrites the schema.
{% /callout %}

## Website and admin: same tables, different RLS

A common mistake is creating pairs of `public_*` (for the site) and `admin_*` (for the panel) tables. **Fayz never does this.** The site side and the admin side read the **same tables** — what changes is the **RLS policy** and the **role** of whoever is querying: the tenant's team sees the tenant's rows; the end customer sees only their own. One table, two views, guaranteed by the database — not two tables to keep in sync.

## Why this matters in practice

This single shape is what makes the CRM's customer and the scheduling booking agree on what "a person" is: both point at the same `public.people`. You don't have to wire any of this by hand — `fayz db apply` creates the layers in the right order (core first, then each plugin's versioned migrations, then your incubator ones) in your own Supabase.

---

Next: provision all of this in a real database with BYOS in [Supabase](/en/docs/data/supabase).
