---
title: "Adding a feature"
description: "Copy the todo resource through rbac, schema, contract, repo/service, RPC, routes, views, and i18n."
canonical_url: "https://vinicunca.dev/taman/guides/adding-a-feature"
---
# Adding a feature

> Copy the todo resource through rbac, schema, contract, repo/service, RPC, routes, views, and i18n.

The todo feature is the reference. Add a new resource the same way, one layer at a time. Names below keep the `@taman/` scope; substitute yours.

### Permissions (`packages/shared/rbac`)

Add the resource to `sharedStatements` in `rbac.shared.ts`. Tuples must stay `as const` so better-auth infers action names:

```ts
export const sharedStatements = {
  todo: ['create', 'read', 'update', 'delete'],
  invoice: ['create', 'read', 'update', 'delete'],
} as const;
```

Grant those actions on the roles that should have them:

- Platform: `rbac.admin.ts` (`adminRole` already grants `todo: […]`; `userRole` does not).
- Organization: `rbac.organizations.ts` (`owner` and `member` both get full todo CRUD). The default organization `admin` role is **dropped** — `roles` replaces better-auth defaults.

Ownership ("this row is yours") is **not** expressed here. Check that in the service.

### Database schema (`packages/server/db-pg`)

Add a table next to `src/schemas/todo.schema.ts`. Export it from `src/schemas/index.ts`.

Todos are org-scoped:

```ts
organizationId: uuid('organization_id')
  .notNull()
  .references(() => organizationTable.id, { onDelete: 'cascade' }),
```

Then `pnpm db-pg:migrate:dev` (`drizzle-kit push` in local dev).

### Contract (`packages/shared/api-contract`)

Add `src/<feature>/<feature>.schema.ts` (Zod) and `<feature>.contract.ts` (`oc.input` / `oc.output`). Mount it on the root contract in `src/index.ts`:

```ts
export const contract = {
  menu: menuContract,
  todo: todoContract,
};
```

Typed errors belong on the contract (`oc.errors({ NOT_FOUND: { message: '…' } })`). Streams use `eventIterator`.

### Repo and service (`apps/api/server/domains/<feature>/`)

- **Repo** — drizzle queries only (`todo.repo.ts`). Fake repo + unit tests exist as the pattern (`todo.repo.fake.ts`).
- **Service** — extends `CoreService` and defines its own private `authorize(action)` that requires a membership, checks `this.can({ <feature>: [action] })`, and returns `member.organizationId` (copy it from `TodoService`; `CoreService` does not provide it). Scope every query by that ID and map rows to contract types. Missing rows return `null`; do not throw `NOT_FOUND` here.

Publishing realtime events stays in the service (`publisher.publish(todoChannel(orgId), event)`).

### RPC procedure (`apps/api/server/rpc/procedures/`)

```ts
const todo = os.todo.use(authed);

export const todoRouter = {
  get: todo.get.handler(async ({ context, input, errors }) => {
    const found = await (await todoService(context)).get(input.id);
    if (!found) {
      throw errors.NOT_FOUND();
    }
    return found;
  }),
  // list, create, update, remove, live
};
```

Register the router in `server/rpc/router.ts`. `authed` enforces a session, and an active organization for non-admins. Platform admins pass `authed` without one, so the service's `authorize` is what keeps their queries scoped.

### Web route and view

1. `apps/web/src/router/routes/modules/<feature>.ts` — default-export a `RouteRecordRaw[]`. Files in `modules/*.ts` (not nested) are eager-loaded. Set `meta.icon`, `meta.order` (`ROUTE_ORDER` in `routes.constants.ts`), `meta.title` via `$t(...)`.
2. Views under `apps/web/src/views/<feature>/`. `generateAccess` maps `import.meta.glob('../views/**/*.vue')`.
3. Call the API with `orpc` / `client` from `src/api/orpc.ts`.

```ts
import { useQuery } from '@tanstack/vue-query';
import { orpc } from '#/api/orpc';

const query = useQuery(orpc.todo.list.queryOptions({
  input: { page: 1, pageSize: 20 },
}));
```

### i18n

Add JSON under `apps/web/src/locales/langs/en-US/<feature>.json` and `id-ID/`. Keys are loaded by `loadLocalesMapFromDir`. Use `$t('todo.title')` in route meta and views.

## Checklist

- [ ] `sharedStatements` + role grants
- [ ] drizzle table, exported, pushed
- [ ] contract schemas + procedures on the root contract
- [ ] repo → service → `os.<feature>.use(authed)` handlers
- [ ] router module + views
- [ ] locale files
- [ ] unit tests beside repo/service/procedure (todo has them)

## What generate strips

The example gallery (`apps/web/src/views/examples/**` and `routes/modules/dev/examples.ts`) is **not** in a generated project. Copy patterns from `todo`, not from those demos, unless you are working in the source repo.


## Sitemap

See the full [sitemap](https://vinicunca.dev/sitemap.md) for all pages.
