@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.
- package/CHANGELOG.md +100 -0
- package/agents/app-section.md +22 -0
- package/agents/standard.md +114 -0
- package/package.json +46 -0
- package/skills/ib-feature/SKILL.md +40 -0
- package/skills/ib-toolkit/SKILL.md +125 -0
- package/skills/ib-ui/SKILL.md +155 -0
- package/src/cli.ts +85 -0
- package/src/create-args.ts +10 -0
- package/src/create.ts +240 -0
- package/src/foundation.ts +42 -0
- package/src/mcp-servers.ts +8 -0
- package/src/mcp.ts +383 -0
- package/src/sync.ts +117 -0
- package/src/template.ts +34 -0
- package/template/.agents/skills/ib-feature/SKILL.md +40 -0
- package/template/.agents/skills/ib-toolkit/SKILL.md +125 -0
- package/template/.agents/skills/ib-ui/SKILL.md +155 -0
- package/template/.dependency-cruiser.cjs +1 -0
- package/template/.env.example +12 -0
- package/template/.github/workflows/ci.yml +14 -0
- package/template/AGENTS.md +139 -0
- package/template/_gitignore +10 -0
- package/template/biome.json +6 -0
- package/template/next.config.ts +32 -0
- package/template/package.json +62 -0
- package/template/postcss.config.mjs +7 -0
- package/template/sentry.edge.config.ts +11 -0
- package/template/sentry.server.config.ts +12 -0
- package/template/src/app/api/health/route.test.ts +12 -0
- package/template/src/app/api/health/route.ts +7 -0
- package/template/src/app/error.tsx +24 -0
- package/template/src/app/global-error.tsx +20 -0
- package/template/src/app/globals.css +4 -0
- package/template/src/app/icon.svg +1 -0
- package/template/src/app/layout.tsx +24 -0
- package/template/src/app/loading.tsx +9 -0
- package/template/src/app/not-found.tsx +14 -0
- package/template/src/app/page.tsx +7 -0
- package/template/src/app/robots.ts +6 -0
- package/template/src/instrumentation-client.ts +14 -0
- package/template/src/instrumentation.ts +13 -0
- package/template/src/shared/config/env.ts +19 -0
- package/template/src/shared/config/routes.ts +3 -0
- package/template/src/test/server-only.ts +1 -0
- package/template/tsconfig.json +10 -0
- 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,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.
|