# Example organisation (/self-hosting/example)



This page starts a local stack with a demo organisation already in place: a project, a database, teams and their access, schemas with row-level security, app users and a branch to merge. It is for trying Chelabase out. For a real instance, see [Install](/self-hosting/install).

<Callout type="warn">
  **Local demo only.** Every account in the demo uses one published password, `Acme-Demo-2026!`. Anyone who can reach the stack can sign in as any of them. Run it on your own machine and do not expose it.

  The seed refuses to run unless the three hosts are `localhost` or `*.localhost` and both the gateway and the Postgres proxy are published only on loopback. It also refuses a stack that requires email verification, and an instance that someone else has already claimed.
</Callout>

## Start it [#start-it]

The steps are the ones in the header of `infra/compose/docker-compose.example.yml`:

```bash
cd infra/compose && cp .env.example .env
docker compose -f docker-compose.yml -f docker-compose.example.yml up -d --wait
docker compose -f docker-compose.yml -f docker-compose.example.yml logs example-seed
```

To run the seed from your checkout's code instead of the published image, build the images first:

```bash
docker compose -f docker-compose.yml -f docker-compose.build.yml build init-secrets studio
docker tag chelabase/server:local chelabase/init:local
CHELABASE_IMAGE_REPO=chelabase CHELABASE_IMAGE_TAG=local docker compose \
  -f docker-compose.yml -f docker-compose.build.yml -f docker-compose.example.yml up -d --wait
```

The seed claims the instance for Ada, so you don't use a setup code by hand. Its log prints the account list. Then open `http://studio.chelabase.localhost:8000` and sign in as any demo person.

If `up` stops part way, run it again: the seed picks up where it stopped. Its last step marks the demo complete with a comment on the `acme` database's `main` branch. After that, every `up` skips the seed and logs that it made 0 writes, so whatever you change in the demo stays changed. That includes unprotecting `main`, renaming the organisation and removing people from a team.

## Reset [#reset]

```bash
cd infra/compose
docker compose -f docker-compose.yml -f docker-compose.example.yml down -v
```

This deletes every database and every secret, the completion mark with them, so the next `up` seeds a fresh demo. Start again from the top afterwards. Computes are not Compose services, so `down -v` leaves them behind. For a full removal, follow the steps in [Stop, tear down and uninstall](/self-hosting/install#stop-tear-down-and-uninstall).

## Who is in it [#who-is-in-it]

Everyone signs in with the password `Acme-Demo-2026!`. Emails are `<first name>@acme.example`, so Bea is `bea@acme.example`. There are 40 people: Ada, and 39 others. Ada is the instance's admin.

The team names below are the Studio names. Members are listed by first name.

| Team        | Level on project Acme | Acts as (Postgres role) | Members                                                                                  |
| ----------- | --------------------- | ----------------------- | ---------------------------------------------------------------------------------------- |
| Engineering | Developer             | `engineering`           | ada, grace, katherine, linus, barbara, dennis, ken, margaret, radia, tim, frances, annie |
| Finance     | Developer             | `finance`               | katherine, rosalind, emmy, joan, luca, blaise, sofia, carl                               |
| Sales       | Developer             | `sales`                 | carl, bea, cecilia, dorothy, elizabeth, florence, gertrude, hypatia, irene, jocelyn      |
| Support     | Viewer                | `support`               | cecilia, lise, maria, nettie, ynes, valentina, sally, mae, annie, henrietta              |
| Leadership  | Admin                 | all roles               | ada, hedy, alan, marie, rachel                                                           |

A Developer runs SQL as one of the Postgres roles its team is granted. A Viewer can browse but not run SQL. An Admin can act as every role. See [Members and invites](/using/members-and-invites) and [Roles and schema isolation](/using/roles-and-schema-isolation).

People can be in more than one team. Carl is in Finance and Sales, so he can act as either role.

**One override.** `alan@acme.example` is an Admin through Leadership, but on the `acme` database he is narrowed to Viewer. The override applies to that database only.

**App users.** `carol@customer.example`, `dave@customer.example` and `erin@customer.example` are customers. They use the same password, and they sign in through the client, not Studio. See [App users](/using/app-users).

## What is in the database [#what-is-in-the-database]

The project is **Acme**, and it has one database, `acme`. Each team works in its own schema, and each schema is owned by its team's Postgres role.

| Schema        | Owned by      | Tables                                 |
| ------------- | ------------- | -------------------------------------- |
| `engineering` | `engineering` | none                                   |
| `finance`     | `finance`     | `finance.invoices`                     |
| `sales`       | `sales`       | `sales.orders`                         |
| `support`     | `support`     | `support.tickets`                      |
| `public`      |               | `public.customers`, `public.employees` |

Row-level security is on for every table except `public.employees`. That table has none on purpose, so Studio's security warning shows it. See [Studio](/using/studio).

* `public.customers`: an app user sees only their own row. The `sales`, `finance` and `support` roles read every customer.
* `sales.orders`: an app user sees only their own orders (`customer_id = chelabase.user_id()`). Sales can read and write every order, and Finance can read them.
* `finance.invoices`: only Finance can reach it.
* `support.tickets`: an app user sees only their own tickets. The `support` role sees the tickets of the EMEA region.
* `public.employees`: no row-level security. Anyone with the public key can read every row through the data API. App users can't change it: the seed revokes the data API's write grants on it, and on `public.customers`.

Each team's tables are owned by the team's role, and an owner normally skips its own table's policies. The seed forces row-level security on `sales.orders`, `finance.invoices` and `support.tickets`, so the policies apply to their owners too.

Deleting an app user deletes their row in `public.users`. Their customer row, orders, invoices and tickets are deleted with it (`on delete cascade`), so deleting a customer in Studio works.

The data API's default schema is `sales`, so `from("orders")` means `sales.orders`. Other tables need their schema, for example `from("public.customers")`. See [TypeScript client](/using/client) and [Data API](/using/data-api).

**Keys.** The `main` branch of database `acme` has the public key and a secret server key named `acme-server`. The seed never prints a secret key. See [Keys](/using/data-api/keys).

**Branches.** `main` is protected. `feature-discounts` is ready to merge. It adds `sales.orders.discount_pct`, a percentage from 0 to 100 that defaults to 0. See [Branches](/using/branches) and [Merging schema](/using/merging-schema).

**Saved queries.** The SQL editor has two, shared across the project: "Revenue by month" and "Open tickets by region". See [SQL editor](/using/sql-editor).

## Things to try [#things-to-try]

* **Write across teams.** Sign in as `bea@acme.example`. In the SQL editor, set **Run as** to `sales` and run:

  ```sql
  insert into finance.invoices (order_id, customer_id, amount, issued_on)
  select id, customer_id, total, current_date from sales.orders limit 1;
  ```

  Postgres refuses it with a permission error, because the `sales` role has no rights on `finance`. Now run the same kind of insert into `sales.orders`, for example `insert into sales.orders (customer_id, total) select id, 10 from public.customers limit 1;`. It works. `main` is protected, so Studio asks you to confirm each change before it runs.

* **Browse without running SQL.** Sign in as `lise@acme.example`. She is in Support, a Viewer. She can browse the tables, but she cannot run SQL.

* **See Support's region rule.** Support members are Viewers, so try it as Ada. Sign in as `ada@acme.example`, set **Run as** to `support` in the SQL editor and run `select subject, region from support.tickets;`. Only the EMEA tickets come back. Run as yourself (the owner) and you see every region.

* **Find the unprotected table.** Open Studio's security warning. It flags `public.employees`, which has no row-level security. Anyone with the public key can read it, but not change it.

* **Read orders as a customer.** Sign in as `carol@customer.example` through the client, and read `sales.orders`. You see only Carol's rows. The client needs the generated types (see [Generated types](/using/client/types)). Replace the key with your public key:

  ```ts
  import { createClient } from "@chelabase/client";
  import { type Database, types } from "./db";

  const db = createClient<Database>({
    url: "http://api.chelabase.localhost:8000",
    key: "chk_pub_…",
    types,
  });

  await db.auth.signIn({ email: "carol@customer.example", password: "Acme-Demo-2026!" });
  const orders = await db.from("orders").select("id", "total", "status", "created_at");
  ```

* **Merge a branch.** Open the `feature-discounts` branch and merge it into `main`. The merge adds `discount_pct` to `sales.orders`. Follow [Merging schema](/using/merging-schema) for the steps.

* **See the override.** Sign in as `alan@acme.example`. He can read the `acme` database but cannot change it. See [Members and invites](/using/members-and-invites).
