---
title: Branches
description: Create branches with unique codes, list them with active filters, and understand how branch scoping isolates data.
---

You model every physical pharmacy as a branch row in `branches`, defined in `packages/database/schema.sql`. The HTTP surface lives in `apps/server/src/features/branches/route.ts` and every route sits behind `authMiddleware`.

### What a branch holds

You work with `id`, `code`, `name`, `city`, `vat_number`, `address`, `currency_code`, `is_active`, and `created_at`. The `code` is your human key, like `B1` or `RIYADH-01`, and it must be unique across the table. Each branch also trades in one currency: `currency_code` defaults to `UGX` and an unknown code is refused by `resolveBranchCurrency` in `apps/server/src/features/branches/service.ts`. Your JWT carries a `branch_id` claim that decides which rows you may touch.

### Listing branches

Any authenticated user lists and reads branches. The list defaults to active branches and paginates with `page` and `page_size`.

```bash
curl "http://localhost:8000/branches?page=1&page_size=50" \
  -H "Authorization: Bearer $TOKEN"
```

You filter with `is_active=true`, `is_active=false`, or `is_active=all`. You fetch one branch directly when you know its id:

```bash
curl http://localhost:8000/branches/BRANCH_ID \
  -H "Authorization: Bearer $TOKEN"
```

A missing branch returns 404 with detail `Branch not found`.

### Creating a branch

Only admins create branches with `POST /branches`. You must send `name`, `code`, and `city`, and you may add `vat_number`, `address`, and `currency_code`. A duplicate code returns 400 with `Branch code already exists`, and an unknown currency returns 400 with `Unknown currency: CODE`.

```bash
curl -X POST http://localhost:8000/branches \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code":"B3","name":"Third Branch","city":"Dammam","vat_number":"300123456700003","address":"King Fahd Rd"}'
```

Creation returns 201 with the new row and emits a `branches` sync change marked `global` visibility, so every device pulls it. You classify visibility in `apps/server/src/core/common/visibility.ts`, and `emitChange` in `apps/server/src/features/sync/protocol/emitter.ts` tags each change with it as it lands in `sync_changes`.

### Updating a branch

Only admins update branches with `PUT /branches/:branchId`. The allowlist is `name`, `city`, `vat_number`, `address`, `is_active`, and `currency_code`, and an empty update returns 400 with `No valid fields to update`. Every update emits another `branches` sync change.

```bash
curl -X PUT http://localhost:8000/branches/BRANCH_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"address":"New address","is_active":true}'
```

### How branch scoping isolates data

You never see another branch sales, returns, sessions, or parked carts. Sales writes force `currentUser.branch_id`, returns reject rows from a different branch with 403, and the pull read in `apps/server/src/features/sync/queries.ts` only returns `branch` visibility rows for your device branch plus `global` rows. Users are `global` entities, so admins manage staff across branches while counter data stays local.

### Deactivating a branch

You retire a branch by setting `is_active` to false with `PUT /branches/:branchId`. Deactivated branches drop out of the default list, disappear from sync snapshots in `apps/server/src/features/sync/route.ts`, and reject new user assignments because `requireActiveBranch` in `apps/server/src/features/users/service.ts` demands `Branch does not exist or is inactive` for an inactive branch.

```bash
curl -X PUT http://localhost:8000/branches/BRANCH_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_active":false}'
```

You reactivate with the same endpoint and `true`. Historical sales, returns, and audit rows keep their original `branch_id`, so reporting still reconciles after deactivation.

### Demo branches and users

Seeding with `bun --cwd packages/database seed` gives you two branches and three logins. Sign in as `admin@demo.pharmaflow`, `pharm1@demo.pharmaflow`, or `pharm2@demo.pharmaflow`, all with password `Demo@1234`. The two pharmacist accounts sit on different branches, so you can prove scoping by listing sales with each token.

### Next steps

Lock down who can do what in /platform/authentication-and-users, trace changes per branch in /platform/audit-log, and fetch branch-filtered deltas in /platform/sync-protocol.
