@inbeat/standards 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/agents/app-section.md +22 -0
  3. package/agents/standard.md +114 -0
  4. package/package.json +46 -0
  5. package/skills/ib-feature/SKILL.md +40 -0
  6. package/skills/ib-toolkit/SKILL.md +125 -0
  7. package/skills/ib-ui/SKILL.md +155 -0
  8. package/src/cli.ts +85 -0
  9. package/src/create-args.ts +10 -0
  10. package/src/create.ts +240 -0
  11. package/src/foundation.ts +42 -0
  12. package/src/mcp-servers.ts +8 -0
  13. package/src/mcp.ts +383 -0
  14. package/src/sync.ts +117 -0
  15. package/src/template.ts +34 -0
  16. package/template/.agents/skills/ib-feature/SKILL.md +40 -0
  17. package/template/.agents/skills/ib-toolkit/SKILL.md +125 -0
  18. package/template/.agents/skills/ib-ui/SKILL.md +155 -0
  19. package/template/.dependency-cruiser.cjs +1 -0
  20. package/template/.env.example +12 -0
  21. package/template/.github/workflows/ci.yml +14 -0
  22. package/template/AGENTS.md +139 -0
  23. package/template/_gitignore +10 -0
  24. package/template/biome.json +6 -0
  25. package/template/next.config.ts +32 -0
  26. package/template/package.json +62 -0
  27. package/template/postcss.config.mjs +7 -0
  28. package/template/sentry.edge.config.ts +11 -0
  29. package/template/sentry.server.config.ts +12 -0
  30. package/template/src/app/api/health/route.test.ts +12 -0
  31. package/template/src/app/api/health/route.ts +7 -0
  32. package/template/src/app/error.tsx +24 -0
  33. package/template/src/app/global-error.tsx +20 -0
  34. package/template/src/app/globals.css +4 -0
  35. package/template/src/app/icon.svg +1 -0
  36. package/template/src/app/layout.tsx +24 -0
  37. package/template/src/app/loading.tsx +9 -0
  38. package/template/src/app/not-found.tsx +14 -0
  39. package/template/src/app/page.tsx +7 -0
  40. package/template/src/app/robots.ts +6 -0
  41. package/template/src/instrumentation-client.ts +14 -0
  42. package/template/src/instrumentation.ts +13 -0
  43. package/template/src/shared/config/env.ts +19 -0
  44. package/template/src/shared/config/routes.ts +3 -0
  45. package/template/src/test/server-only.ts +1 -0
  46. package/template/tsconfig.json +10 -0
  47. package/template/vitest.config.ts +14 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,100 @@
1
+ # Changelog
2
+
3
+ ## [0.7.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.6.1...standards-v0.7.0) (2026-10-01)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * consumers must drop the @inbeat registry line from .npmrc/bunfig and the NODE_AUTH_TOKEN setup.
9
+
10
+ ### Features
11
+
12
+ * publish [@inbeat](https://github.com/inbeat) packages publicly on npm ([#27](https://github.com/Genoux/ib-toolkit/issues/27)) ([2bd9f53](https://github.com/Genoux/ib-toolkit/commit/2bd9f53c2d4dd030bdee037b8b1dce1e1bb828b5))
13
+
14
+ ## [0.6.1](https://github.com/Genoux/ib-toolkit/compare/standards-v0.6.0...standards-v0.6.1) (2026-10-01)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **standards:** ship template changes in standards releases ([#25](https://github.com/Genoux/ib-toolkit/issues/25)) ([176a543](https://github.com/Genoux/ib-toolkit/commit/176a543499eac56cc131595bb9c011c6664f06cc))
20
+
21
+ ## [0.6.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.5.0...standards-v0.6.0) (2026-10-01)
22
+
23
+
24
+ ### ⚠ BREAKING CHANGES
25
+
26
+ * remove app logic and finish the toolkit ([#21](https://github.com/Genoux/ib-toolkit/issues/21))
27
+
28
+ ### Bug Fixes
29
+
30
+ * **config:** require biome 2.5 and accept biome.jsonc ([#19](https://github.com/Genoux/ib-toolkit/issues/19)) ([178dc95](https://github.com/Genoux/ib-toolkit/commit/178dc95b4c9a041970458526c3985f5a225a7caf))
31
+
32
+
33
+ ### Code Refactoring
34
+
35
+ * remove app logic and finish the toolkit ([#21](https://github.com/Genoux/ib-toolkit/issues/21)) ([cecf25a](https://github.com/Genoux/ib-toolkit/commit/cecf25a670b2177f8b1e8cc3645023a1219a2626))
36
+
37
+ ## [0.5.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.4.0...standards-v0.5.0) (2026-10-01)
38
+
39
+
40
+ ### ⚠ BREAKING CHANGES
41
+
42
+ * **standards:** `ib add` is removed (use the vendor init CLIs); `ib check` now requires the shared config presets.
43
+
44
+ ### Features
45
+
46
+ * **standards:** drop add-ons, add ib mcp, enforce shared presets ([#17](https://github.com/Genoux/ib-toolkit/issues/17)) ([9eb8410](https://github.com/Genoux/ib-toolkit/commit/9eb84108432dda5e2a2da312a7bc70299e09c0c9))
47
+
48
+ ## [0.4.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.3.0...standards-v0.4.0) (2026-09-30)
49
+
50
+
51
+ ### ⚠ BREAKING CHANGES
52
+
53
+ * **standards:** sync skills to .agents/skills only ([#15](https://github.com/Genoux/ib-toolkit/issues/15))
54
+
55
+ ### Features
56
+
57
+ * **standards:** sync skills to .agents/skills only ([#15](https://github.com/Genoux/ib-toolkit/issues/15)) ([cd3e8a2](https://github.com/Genoux/ib-toolkit/commit/cd3e8a21f028790ad84c26dc1cc832475885c461))
58
+
59
+ ## [0.3.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.2.0...standards-v0.3.0) (2026-09-30)
60
+
61
+
62
+ ### ⚠ BREAKING CHANGES
63
+
64
+ * **standards:** create always makes the base app, add-ons only via ib add ([#13](https://github.com/Genoux/ib-toolkit/issues/13))
65
+
66
+ ### Features
67
+
68
+ * **standards:** create always makes the base app, add-ons only via ib add ([#13](https://github.com/Genoux/ib-toolkit/issues/13)) ([5f7e0c3](https://github.com/Genoux/ib-toolkit/commit/5f7e0c38a49b0f5dafd3dd9ae6472df03067f830))
69
+ * **standards:** point any ai tool at the guides and the brief ([#12](https://github.com/Genoux/ib-toolkit/issues/12)) ([b419b4a](https://github.com/Genoux/ib-toolkit/commit/b419b4ad8e11a60ee704b4435add702fd9bd78b2))
70
+
71
+ ## [0.2.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.1.2...standards-v0.2.0) (2026-09-30)
72
+
73
+
74
+ ### ⚠ BREAKING CHANGES
75
+
76
+ * **ui,next:** harden ui peers, csp and rate-limit ip ([#8](https://github.com/Genoux/ib-toolkit/issues/8))
77
+
78
+ ### Features
79
+
80
+ * add ib create and ib add ([#9](https://github.com/Genoux/ib-toolkit/issues/9)) ([aef8b6e](https://github.com/Genoux/ib-toolkit/commit/aef8b6e161ac66167b69d67ae279983976b17b09))
81
+
82
+
83
+ ### Bug Fixes
84
+
85
+ * **standards:** keep the version out of the managed block ([#11](https://github.com/Genoux/ib-toolkit/issues/11)) ([43efd08](https://github.com/Genoux/ib-toolkit/commit/43efd081446d8cd836b1de68f0049ecacf45d872))
86
+ * **ui,next:** harden ui peers, csp and rate-limit ip ([#8](https://github.com/Genoux/ib-toolkit/issues/8)) ([4ad992c](https://github.com/Genoux/ib-toolkit/commit/4ad992c96181a21c3dbee0bb542d9afe32163deb))
87
+
88
+ ## [0.1.2](https://github.com/Genoux/ib-toolkit/compare/standards-v0.1.1...standards-v0.1.2) (2026-09-30)
89
+
90
+
91
+ ### Bug Fixes
92
+
93
+ * resolve workspace ranges from manifests on publish ([#5](https://github.com/Genoux/ib-toolkit/issues/5)) ([0f4328e](https://github.com/Genoux/ib-toolkit/commit/0f4328eb0897501380880f429a952d9d228563a2))
94
+
95
+ ## [0.1.1](https://github.com/Genoux/ib-toolkit/compare/standards-v0.1.0...standards-v0.1.1) (2026-09-30)
96
+
97
+
98
+ ### Bug Fixes
99
+
100
+ * ship compiled js from core and next ([#3](https://github.com/Genoux/ib-toolkit/issues/3)) ([8fef50a](https://github.com/Genoux/ib-toolkit/commit/8fef50aa87679bcf3c328fe4ca11a351434e2dda))
@@ -0,0 +1,22 @@
1
+ ## This app
2
+
3
+ ### Product brief
4
+
5
+ -
6
+
7
+ ### Stack deltas
8
+
9
+ -
10
+
11
+ ### Deliberate deviations
12
+
13
+ | What | Why | Owner |
14
+ |---|---|---|
15
+
16
+ ### Known gaps
17
+
18
+ -
19
+
20
+ ### Learned facts
21
+
22
+ -
@@ -0,0 +1,114 @@
1
+ ## inBeat toolkit standard
2
+
3
+ Stack: Next.js App Router, TypeScript strict, React 19, Drizzle + Neon, Clerk, Sentry, Vercel, Bun.
4
+ Shared code comes from `@inbeat/*` (ui ships source via `transpilePackages`; core and next ship compiled JS). Never fork a toolkit
5
+ file into the app; change it in ib-toolkit.
6
+
7
+ ### Before you build
8
+
9
+ - If "Product brief" under "This app" is empty, write the user's brief there first (problem,
10
+ users, core flows), then build.
11
+ - Read the matching guide before the work, whatever tool you are: `.agents/skills/ib-ui/SKILL.md`
12
+ (component catalog, UI rules) before any UI, `.agents/skills/ib-feature/SKILL.md` for a new
13
+ feature, `.agents/skills/ib-toolkit/SKILL.md` for actions, routes, auth, env and rate limits.
14
+ - Read the version-matched Next.js docs in `node_modules/next/dist/docs/` before Next.js work.
15
+
16
+ ### Two tiers
17
+
18
+ - **Required** (CI enforces): `@inbeat/config` presets (extend them: biome, tsconfig, dependency-cruiser, vitest; `ib check`
19
+ enforces it), the action/route error contract,
20
+ `securityHeaders()`, Sentry presets, rate limiting on public entry points, this block.
21
+ - **Opt-in**: `@inbeat/ui` blocks, extra layer rules.
22
+ - A deliberate departure is allowed when the app states it under **Deliberate deviations**
23
+ in its own section of this file (what, why, owner). Undeclared drift is a bug.
24
+
25
+ ### Layout
26
+
27
+ ```
28
+ src/app/ routes + composition; private pieces in _components/
29
+ src/features/<f>/actions/ "use server" mutations, one per file, via action()
30
+ src/features/<f>/queries/ server-only reads for Server Components
31
+ src/features/<f>/server/ domain commands and policies ("server-only", no "use server")
32
+ src/features/<f>/components/ hooks/ lib/ schemas.ts types.ts
33
+ src/entities/ leaf: domain types, zod schemas, constants
34
+ src/integrations/<vendor>/ external adapters
35
+ src/shared/{components,config,lib,hooks}/
36
+ src/db/schema/<table>.ts, src/db/migrations/
37
+ ```
38
+
39
+ Layers (dependency-cruiser, `@inbeat/config/dependency-cruiser`): features never import each
40
+ other; entities import nothing internal; db imports entities only; shared and integrations never
41
+ import features or app.
42
+
43
+ ### Reads and writes
44
+
45
+ | Need | Use |
46
+ |---|---|
47
+ | Page data | Server Component calls `queries/*` |
48
+ | Mutation from own UI | `action()` from `@inbeat/next/action`, returns `Result` |
49
+ | Client polling, infinite lists | `GET` via `route()`, React Query |
50
+ | Webhooks, presign, redirects, cron | `route()` |
51
+
52
+ Server actions are queued per client; never use them for reads.
53
+
54
+ ### Errors
55
+
56
+ - Actions resolve to `{ ok: true, data } | { ok: false, error, code?, errorId? }`.
57
+ Routes answer `{ error, code, errorId? }` with the status from `APP_ERROR_STATUS`.
58
+ - Throw `AppError(kind)` from `@inbeat/core/errors`; never `new Error("Forbidden")`.
59
+ Kinds: validation, auth, forbidden, not_found, conflict, rate_limited, db_transient,
60
+ db_query, timeout, external_service, unknown.
61
+ - Unexpected errors are classified, reported once, and replaced with safe copy + `ERR_xxxx`.
62
+ - Log with `createLogger()`; ids only, never emails, names or payloads. No `console.log`.
63
+
64
+ ### Auth
65
+
66
+ - New apps start without authentication; set it up per "Integrations" in the `ib-toolkit` skill.
67
+ - `authorize` is required on every `action()` and `route()`, and it is the app's own check
68
+ (session, roles, ownership). `publicAccess` is an explicit choice and must be rate limited.
69
+ - Redirect targets go through `safeRedirectPath`. `APP_URL` comes from env, never `Host`.
70
+
71
+ ### Security baseline
72
+
73
+ - `securityHeaders()` in `next.config.ts`; CSP report-only until a clean week, then enforce.
74
+ - `createRateLimiter()` (Upstash in prod) on every unauthenticated entry, presign and LLM call.
75
+ - Webhooks verify the sender's signature. Secrets only in Vercel env.
76
+ - `import "server-only"` in every module touching secrets or the DB.
77
+ - Env through `@t3-oss/env-nextjs` + `@inbeat/core/env` fragments; no `process.env` elsewhere.
78
+
79
+ ### Data
80
+
81
+ - UUID PKs, `timestamp({ withTimezone: true })`, snake_case, one table per file.
82
+ - Migrations are generated, reviewed, forward-only, expand then contract; they run after the
83
+ build resolves. Never run migrations or destructive SQL against production unless the user
84
+ says "apply migrations to prod".
85
+ - Deletes that own storage objects collect keys in the transaction and delete after commit.
86
+
87
+ ### UI
88
+
89
+ - UI comes only from `@inbeat/ui` (catalog in the `ib-ui` skill) plus layouts composed from it. If a
90
+ component or variant is missing, stop and flag it for ib-toolkit; never invent, copy or patch
91
+ one (a workaround needs the user's OK and a `// toolkit-gap:` comment).
92
+ - Primitives and generic blocks from `@inbeat/ui/components/*`; tokens from
93
+ `@inbeat/ui/theme.css`. A block moves to the toolkit only once an app actually uses it.
94
+ - Buttons are `rounded-full` with no destructive variant; destructive flows use
95
+ `DestructiveActionDialog` from `@inbeat/ui`. Icon-only buttons get a tooltip. Lists use
96
+ `DataTableShell` + `DataTable`; empty lists use `EmptyState`.
97
+ - Radix via `radix-ui`; animation via `motion` only.
98
+
99
+ ### Simplicity
100
+
101
+ - Reuse before writing: the `ib-ui` catalog, `@inbeat/*`, the stdlib, then the platform. No speculative abstractions, config or "for later" scaffolding.
102
+ - Comments carry only what code cannot. A deliberate shortcut is marked `// shortcut: <ceiling>; <upgrade path>`.
103
+ - Claude Code reads `AGENTS.md` only when the app has no `CLAUDE.md`; if one exists it must contain an `@AGENTS.md` line (`ib check` fails otherwise).
104
+
105
+ ### Tests and CI
106
+
107
+ - Vitest colocated `*.test.ts` for every `server/` command, policy and `lib/` function;
108
+ `*.integration.test.ts` against a Neon branch; Playwright in `tests/e2e` against previews.
109
+ - CI: biome, tsc, dependency-cruiser, vitest, migration check, `ib check`, gitleaks.
110
+
111
+ ### Commits
112
+
113
+ Conventional commits, lowercase, present tense, title under 60 chars. No AI attribution.
114
+ Never commit, push or open a PR unless explicitly asked.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@inbeat/standards",
3
+ "version": "0.7.0",
4
+ "description": "Agent standards for inBeat apps: managed AGENTS.md block, skills under .agents/skills, ib CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "ib": "./src/cli.ts"
8
+ },
9
+ "files": [
10
+ "CHANGELOG.md",
11
+ "src",
12
+ "agents",
13
+ "skills",
14
+ "template",
15
+ "!src/**/*.test.ts",
16
+ "!template/**/node_modules",
17
+ "!template/.next",
18
+ "!template/.turbo",
19
+ "!template/coverage",
20
+ "!template/.vercel",
21
+ "!template/next-env.d.ts",
22
+ "!template/*.tsbuildinfo",
23
+ "!template/**/.env",
24
+ "!template/**/.env.[!e]*"
25
+ ],
26
+ "exports": {
27
+ "./sync": "./src/sync.ts"
28
+ },
29
+ "scripts": {
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "bun --bun vitest run"
32
+ },
33
+ "devDependencies": {
34
+ "@inbeat/config": "^0.7.0",
35
+ "vitest": "^4.1.1"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/Genoux/ib-toolkit.git",
40
+ "directory": "packages/standards"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "license": "UNLICENSED"
46
+ }
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: ib-feature
3
+ description: Scaffold or extend a feature slice in an inBeat app following the toolkit layout (actions, queries, server, components, schemas, tests). Use when adding a new feature, entity, page with data, or when the user mentions ib-feature or @ib-feature.
4
+ ---
5
+
6
+ # ib-feature
7
+
8
+ ## Steps
9
+
10
+ 1. **Schema.** Table in `src/db/schema/<table>.ts` (uuid pk, `timestamp({ withTimezone: true })`,
11
+ snake_case). Export from `src/db/schema/index.ts`. `bun run db:generate`, then read the SQL.
12
+ Never apply to production unless the user says "apply migrations to prod".
13
+ 2. **Entities.** Shared types, zod schemas, constants in `src/entities/<name>.ts` if more than
14
+ one feature needs them; otherwise `src/features/<f>/schemas.ts` and `types.ts`.
15
+ 3. **Server.** `src/features/<f>/server/<verb>-<noun>-record.ts` with `import "server-only"`.
16
+ Pure DB/domain logic, takes validated input plus the viewer id, returns data or throws
17
+ `AppError`. Policies: `assert-can-<verb>.ts`.
18
+ 4. **Queries.** `src/features/<f>/queries/get-<noun>.ts` / `list-<nouns>.ts`, `import "server-only"`,
19
+ run the app's own authorize check first when the app has auth, called from Server Components.
20
+ 5. **Actions.** `src/features/<f>/actions/<verb>-<noun>.ts` with `"use server"` and `action()`.
21
+ One export per file. `revalidate` the pages that show the data.
22
+ 6. **Routes** only for client polling, infinite lists, webhooks, presign: `route()` in
23
+ `src/app/api/<f>/.../route.ts`.
24
+ 7. **UI.** Page in `src/app/.../page.tsx` (Server Component) renders feature components from
25
+ `src/features/<f>/components`. Client leaves call actions and handle `Result`.
26
+ 8. **Tests.** Colocated `*.test.ts` for every `server/` command and policy and every `lib/` helper, including `src/shared/lib`.
27
+ 9. **Check.** `bun run lint && bunx tsc --noEmit && bun run test && bun run lint:arch`.
28
+
29
+ ## Naming
30
+
31
+ - Files kebab-case, one operation per file, named exports, no barrels (except `db/schema`).
32
+ - Actions verb+noun (`createClient`), queries `get*`/`list*`, server `*Record`,
33
+ policies `assertCan*`, hooks `use-*`.
34
+ - Indexes `<table>_<cols>_idx`, uniques `<table>_<cols>_unique`, FKs `*_user_id`.
35
+
36
+ ## Don't
37
+
38
+ - Import another feature. Move the shared piece to `entities/` or `shared/`.
39
+ - Read data through a server action.
40
+ - Put `"use server"` on a file under `server/`.
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: ib-toolkit
3
+ description: Use the inBeat toolkit (@inbeat/core, @inbeat/next, @inbeat/config) correctly in an inBeat app. Use when writing server actions, route handlers, auth checks, env, rate limits, webhooks, Sentry or logging, or when the user mentions ib-toolkit or @ib-toolkit.
4
+ ---
5
+
6
+ # ib-toolkit
7
+
8
+ Read the "inBeat toolkit standard" block in `AGENTS.md` first. This skill is the how-to. Examples use `authorizeStaff` as a stand-in for the app's own `authorize` check; public endpoints use `publicAccess` plus a rate limiter.
9
+
10
+ ## Server action
11
+
12
+ ```ts
13
+ "use server";
14
+ import { ok } from "@inbeat/core/result";
15
+ import { action } from "@inbeat/next/action";
16
+ import { authorizeStaff } from "@/shared/lib/auth";
17
+ import { createClientSchema } from "../schemas";
18
+ import { createClientRecord } from "../server/create-client-record";
19
+
20
+ export const createClient = action({
21
+ name: "clients.create",
22
+ schema: createClientSchema,
23
+ authorize: authorizeStaff,
24
+ run: async (input, viewer) => ok(await createClientRecord(input, viewer.userId)),
25
+ revalidate: () => ["/clients"],
26
+ });
27
+ ```
28
+
29
+ Client side: `const result = await createClient(values); if (!result.ok) toast.error(result.error);`
30
+
31
+ Public action: `authorize: publicAccess`, then first thing in `run`:
32
+ `await rateLimiter.enforce(\`apply:${clientIp(await headers())}\`, { limit: 5, window: "10 m" })`.
33
+ The limiter is one shared instance in `src/shared/lib/rate-limit.ts` (`createRateLimiter({ prefix, upstash })`
34
+ with the `upstashEnv` values) imported by every action and route. Get the IP only from `clientIp` in
35
+ `@inbeat/next/rate-limit` (routes pass `request.headers`); never write your own header parsing.
36
+ `clientIp` is reliable on Vercel only; self-hosted apps behind a proxy pass `{ trustedProxyHops: n }`,
37
+ otherwise it returns `"unknown"`. Public forms using Turnstile without Clerk must add its hosts via
38
+ `securityHeaders({ sources: { script: ["https://challenges.cloudflare.com"], frame: ["https://challenges.cloudflare.com"] } })`.
39
+
40
+ ## Route handler
41
+
42
+ ```ts
43
+ import { route } from "@inbeat/next/route";
44
+ export const GET = route({
45
+ name: "clients.list",
46
+ authorize: authorizeStaff,
47
+ query: z.object({ cursor: z.string().optional() }),
48
+ handler: async ({ query }) => listClients(query.cursor),
49
+ });
50
+ ```
51
+
52
+ ## Errors
53
+
54
+ - Deny: `throw new AppError("forbidden", "reason for logs")`. The user sees safe default copy.
55
+ - Expected business failure: `return fail("That handle is taken", "conflict")`.
56
+ - Anything else: let it throw. The wrapper classifies, reports to Sentry and returns `ERR_xxxx`.
57
+ - Postgres: `isUniqueViolation(err)` from `@inbeat/core/errors`, never string matching.
58
+
59
+ ## Authorization
60
+
61
+ `authorize` is the app's own check: it returns the viewer or throws `AppError("auth" | "forbidden")`. Roles, admin rules and ownership are app decisions; write them in the app (for example `src/shared/lib/auth.ts`) and pass them to `action()` and `route()`.
62
+
63
+ ## Integrations
64
+
65
+ Sentry is already wired in the template.
66
+
67
+ Run `ib mcp` to give your AI tool access to Vercel, GitHub, Sentry, Neon, Clerk, Cloudflare.
68
+
69
+ ### Auth (Clerk)
70
+
71
+ 1. Run `npx -y clerk@latest init` (works with `bunx`). It detects Next.js and applies Clerk's setup; `npx -y clerk@latest doctor` verifies it.
72
+ 2. In `proxy.ts`, exclude `/monitoring` (the Sentry tunnel) from the matcher, and keep `/api/health` and `/robots.txt` public.
73
+ 3. Pass `clerkFrontendApi(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY)` to `securityHeaders` (the base `next.config.ts` already does).
74
+ 4. Add `clerkServerEnv` and `clerkClientEnv` from `@inbeat/core/env` to `src/shared/config/env.ts` (see Env below).
75
+
76
+ Access pattern. Clerk's development and production instances are configured separately: apply every
77
+ dashboard step to both.
78
+
79
+ - Who gets in: a staff-only app turns on the Clerk allowlist (`*@inbeat.agency`, …). It only blocks
80
+ new sign-ups, so review existing users once after enabling it. Apps with external users keep
81
+ sign-ups open and rely on roles.
82
+ - Roles: `publicMetadata.role`, assigned in the Clerk dashboard (users cannot edit it). Expose it
83
+ with the session token claim `{"metadata": "{{user.public_metadata}}"}`, read it from
84
+ `sessionClaims.metadata.role`. No role or an unknown one means no access.
85
+ - Checks run on the server, twice: the proxy requires sign-in outside the public routes and blocks
86
+ role-gated areas; every role-gated page, action and route checks again (`authorize`). Hiding UI
87
+ is not access control.
88
+ - Stays in the app: users table (if joins need it), ownership ("owner or admin"), rate limits,
89
+ maintenance mode.
90
+ - Deviation: deriving a role from a verified email domain is allowed when the app documents it in
91
+ AGENTS.md "Deliberate deviations".
92
+
93
+ ### Database (Neon + Drizzle)
94
+
95
+ 1. Run `npx neon@latest init` to link the Neon project and install the agent tooling.
96
+ 2. Set up Drizzle per its Neon guide: `drizzle-orm`, `@neondatabase/serverless`, `drizzle-kit`, and a `drizzle.config.ts` with `dialect: "postgresql"`.
97
+ 3. Conventions from the standard's Data section: tables in `src/db/schema/<table>.ts`, UUID primary keys, migrations generated with `drizzle-kit generate` and reviewed, and never run migrations or destructive SQL against production.
98
+
99
+ ## Env
100
+
101
+ ```ts
102
+ import { createEnv } from "@t3-oss/env-nextjs";
103
+ import { clerkClientEnv, clerkServerEnv, dbEnv, upstashEnv } from "@inbeat/core/env";
104
+ export const env = createEnv({
105
+ server: { ...dbEnv, ...clerkServerEnv, ...upstashEnv },
106
+ client: { ...clerkClientEnv },
107
+ experimental__runtimeEnv: { NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY },
108
+ emptyStringAsUndefined: true,
109
+ });
110
+ ```
111
+
112
+ ## Other building blocks
113
+
114
+ | Need | Import |
115
+ |---|---|
116
+ | Neon pool | `createDb` from `@inbeat/next/db` |
117
+ | Clerk webhook | `createClerkWebhookHandler` from `@inbeat/next/clerk-webhook` |
118
+ | Headers/CSP | `securityHeaders`, `clerkFrontendApi` from `@inbeat/next/security-headers` |
119
+ | Sentry | `sentryServerOptions` from `@inbeat/next/sentry` (server, edge); `sentryClientOptions` from `@inbeat/next/sentry-client` (browser only) |
120
+ | Redirect param | `safeRedirectPath` from `@inbeat/next/safe-redirect` |
121
+
122
+ ## Changing the toolkit
123
+
124
+ Edit `~/Desktop/@inbeat/ib-toolkit`, run its tests, then `bun run sync <app-dir>` there to copy
125
+ the sources into the app's `node_modules/@inbeat/*` (use `--watch` while iterating).
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: ib-ui
3
+ description: Build UI in an inBeat app with @inbeat/ui primitives and tokens. Use when creating or editing components, pages, dialogs, tables, forms, empty states, or when the user mentions ib-ui or @ib-ui.
4
+ ---
5
+
6
+ # ib-ui
7
+
8
+ ## Before writing any UI
9
+
10
+ Check the [Catalog](#catalog). Build pages and domain compositions only from catalog components
11
+ and tokens. Icons are always `lucide-react`.
12
+
13
+ **Allowed in the app**: page layouts and domain compositions (a `CreatorCard` from `Avatar` +
14
+ `Badge`) in `src/shared/components/blocks` or the feature's `components/`. Different apps may
15
+ compose different layouts.
16
+
17
+ **Not allowed**: new generic primitives or variants (a local button, modal, select, table or
18
+ toast), copying or restyling a toolkit component, faking a variant with override classes,
19
+ installing another UI library, running `shadcn add`.
20
+
21
+ **Missing component**: stop. Tell the user which component or variant is missing and why, and
22
+ propose adding it to ib-toolkit (`packages/ui` plus its story). Only with the user's explicit
23
+ agreement, use a workaround and mark it `// toolkit-gap: <what is missing>` so it is greppable.
24
+
25
+ ## Rules
26
+
27
+ - Import `@inbeat/ui/components/<name>`; `cn` from `@inbeat/ui/lib/utils`.
28
+ - Tokens only: `bg-background`, `text-muted-foreground`, `border-border`, `bg-primary`, radius
29
+ scale `rounded-{sm..4xl}`, shadows `shadow-hairline|subtle|elevated`. No hex colors, no
30
+ arbitrary spacing unless no token fits.
31
+ - App shells: `AppShell` + `AppNav`, never Sidebar primitives. Links come in via `renderLink`;
32
+ app sidebar content goes in the `sidebar` slot. Products differ only through `AppShell` props
33
+ (`variant`, `collapsible`, `sidebarWidth`, `headerVariant`), never classes or local shell markup.
34
+ - Variants are only the ones an app uses. `Button`: default, secondary, outline, ghost, link,
35
+ dots; sizes xs, sm, default, lg, icon*; always `rounded-full`, no destructive variant.
36
+ `Badge`: default, secondary, outline. `TabsList` is pills only.
37
+ - Tabs: `Tabs` + `TabsList tabs={[{ value, label, count? }]}` + `TabsContent`; drop
38
+ `TabsContent` for a filter bar.
39
+ - Destructive flows open `DestructiveActionDialog` (owns the confirm step; `variant="typed"`
40
+ makes the user type `DELETE <name>`). Confirm never closes it: call `onOpenChange(false)` on
41
+ success, so a pending or failed action keeps it open.
42
+ - Action menus: `Button variant="dots"` + `DropdownMenu`. Inside an element with `data-row` it
43
+ reveals on row hover; elsewhere it stays visible.
44
+ - Icon-only buttons are `IconButton` (required `label` = tooltip + `aria-label`), except `dots`.
45
+ It works as a `DropdownMenuTrigger asChild` child; with `asChild` it renders a link. Show/hide
46
+ toggles: `label="Show filters" pressedLabel="Hide filters" pressed={isOpen}`; `pressed` owns the
47
+ look. Menu and popover triggers are not toggles: static label, no `pressed`.
48
+ - Labels are one to three words, no ids or emails.
49
+ - Lists: `DataTableShell` + `DataTable`. With no data render `EmptyState` instead of the table;
50
+ DataTable's "No results." is only for an empty filter result.
51
+ - Forms (client leaf): `schema.safeParse(values)` with the schema shared with the action; on
52
+ failure keep one message per field from `issues` (`issue.path[0]`). Per field:
53
+ `<Field data-invalid={Boolean(errors.name)}><Label/><Input aria-invalid={Boolean(errors.name)}
54
+ aria-describedby="name-error"/><FieldError id="name-error">{errors.name}</FieldError></Field>`.
55
+ On success `const result = await action(parsed.data)`; `if (!result.ok) toast.error(result.error)`
56
+ (`sonner`; `result.errorId` when present is the support reference).
57
+ - Animation: `motion/react` only, respecting `prefers-reduced-motion`.
58
+ - `page.tsx` stays a Server Component (title, layout, data loading); only the stateful leaf (a
59
+ table with dialogs, a form) is `"use client"`.
60
+
61
+ ## Catalog
62
+
63
+ Every module of `@inbeat/ui`, imported as `@inbeat/ui/<path>`.
64
+
65
+ ### Actions
66
+ - `components/button` — Button with variants, sizes and `buttonVariants` for link styling.
67
+ - `components/icon-button` — icon-only Button with a required label, tooltip and toggle state.
68
+
69
+ ### Forms
70
+ - `components/field` — Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet and friends for form layout.
71
+ - `components/label` — accessible form Label.
72
+ - `components/input` — text Input.
73
+ - `components/textarea` — multi-line Textarea.
74
+ - `components/checkbox` — Checkbox.
75
+ - `components/select` — single-choice Select.
76
+ - `components/combobox` — searchable Combobox with chips for multiple values.
77
+ - `components/multi-select-popover` — popover multi-select over a list of options.
78
+ - `components/toggle-pills` — pill buttons toggling a set of string options, for filters.
79
+ - `components/input-group` — Input or Textarea with inline addons, buttons and text.
80
+ - `components/search-input` — controlled search Input with a loading state.
81
+ - `components/calendar` — date Calendar (react-day-picker) in the toolkit style.
82
+
83
+ ### Display
84
+ - `components/avatar` — Avatar, AvatarImage, AvatarFallback, AvatarGroup and AvatarGroupCount.
85
+ - `components/person-identity` — PersonAvatar, PersonName and PersonIdentity for a person's image and name.
86
+ - `components/member-avatar-stack` — overlapping avatars of up to three members plus a remainder count.
87
+ - `components/badge` — Badge chip.
88
+ - `components/number-dot` — small count pill, renders nothing at zero.
89
+ - `components/labeled-field` — read-only label and value pair.
90
+ - `components/empty-state` — EmptyState with icon, title, description and an optional labelled action.
91
+ - `components/separator` — horizontal or vertical Separator.
92
+ - `components/carousel` — Embla Carousel, CarouselContent and CarouselItem.
93
+ - `components/collapsible-section` — animated CollapsibleSection with trigger and content.
94
+
95
+ ### Feedback
96
+ - `components/alert` — Alert, AlertTitle and AlertDescription (default, destructive).
97
+ - `components/sonner` — Toaster for `sonner` toasts; mount once in the root layout.
98
+ - `components/spinner` — Spinner and SectionSpinner for loading states.
99
+ - `components/skeleton` — Skeleton placeholder.
100
+ - `components/progress` — Progress bar.
101
+ - `components/platform-disclaimer` — dismissible notice whose dismissal is stored in a cookie.
102
+
103
+ ### Navigation
104
+ - `components/app-nav` — sidebar navigation list driven by `items` and `renderLink`.
105
+ - `components/nav-user` — sidebar user menu with name, email, avatar and dropdown.
106
+ - `components/breadcrumb` — Breadcrumb trail.
107
+ - `components/tabs` — Tabs, TabsList (pills from a `tabs` array) and TabsContent.
108
+ - `components/sidebar` — Sidebar primitives that AppShell is built from; apps do not use them directly.
109
+
110
+ ### Overlays
111
+ - `components/dialog` — modal Dialog.
112
+ - `components/alert-dialog` — AlertDialog for interrupting confirmations.
113
+ - `components/destructive-action-dialog` — confirm step for destructive actions, optionally typed.
114
+ - `components/sheet` — side Sheet panel.
115
+ - `components/popover` — Popover.
116
+ - `components/dropdown-menu` — DropdownMenu.
117
+ - `components/tooltip` — Tooltip and TooltipProvider.
118
+ - `components/portal-container` — PortalContainerProvider and usePortalContainer to portal overlays into a chosen element.
119
+
120
+ ### Data
121
+ - `components/data-table` — DataTable driven by TanStack `columns` and `data`.
122
+ - `components/data-table-shell` — scrollable wrapper that holds a DataTable.
123
+ - `components/data-table-sortable-header` — sortable column header for DataTable columns.
124
+ - `components/table` — Table, TableHeader, TableBody, TableRow, TableHead and TableCell primitives.
125
+ - `components/load-more-sentinel` — invisible marker that triggers infinite loading.
126
+
127
+ ### Layout
128
+ - `components/app-shell` — AppShell page frame with sidebar and header, and AppShellLogo.
129
+
130
+ ### Hooks and lib
131
+ - `hooks/use-mobile` — `useIsMobile()` breakpoint hook.
132
+ - `lib/utils` — `cn()` class merging.
133
+ - `lib/easing` — `EASING_FUNCTION` curves shared with motion animations.
134
+ - `lib/initials` — `getInitials(name, fallback)`.
135
+
136
+ ## Setup in an app
137
+
138
+ ```css
139
+ /* src/app/globals.css */
140
+ @import "tailwindcss";
141
+ @import "tw-animate-css";
142
+ @import "shadcn/tailwind.css";
143
+ @import "@inbeat/ui/theme.css";
144
+ ```
145
+
146
+ `next.config.ts`: `transpilePackages: ["@inbeat/ui"]` (ui ships source; core and next ship compiled JS).
147
+ The template installs every peer the catalog needs.
148
+
149
+ ## Adding or changing a component
150
+
151
+ Do it in `ib-toolkit/packages/ui/src/components`, add or update its story in `packages/ui/stories`
152
+ (`bun run storybook`; stories are grouped like the catalog, render every variant and size from a
153
+ declared list, and use the neutral data in `stories/lib/fixtures.ts`), add it to the Catalog
154
+ above (a test enforces this), then `bun run sync <app-dir>`. Domain compositions tied to one app's
155
+ data stay in the app; once a second app needs one, promote it here.