@tulipes/cli 0.1.1 → 0.2.0-rc.1
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/README.md +10 -7
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +29 -0
- package/dist/agent.js.map +1 -0
- package/dist/app-core.d.ts +8 -6
- package/dist/app-core.js +23 -14
- package/dist/app-core.js.map +1 -1
- package/dist/bin.js +3 -3
- package/dist/bin.js.map +1 -1
- package/dist/config-check.d.ts +8 -0
- package/dist/config-check.js +43 -0
- package/dist/config-check.js.map +1 -0
- package/dist/config-import.d.ts +20 -0
- package/dist/config-import.js +133 -0
- package/dist/config-import.js.map +1 -0
- package/dist/dev.d.ts +4 -4
- package/dist/dev.js +7 -10
- package/dist/dev.js.map +1 -1
- package/dist/init.d.ts +6 -11
- package/dist/init.js +11 -6422
- package/dist/init.js.map +1 -1
- package/dist/inspection.d.ts +9 -8
- package/dist/inspection.js +10 -11
- package/dist/inspection.js.map +1 -1
- package/dist/main.js +82 -11
- package/dist/main.js.map +1 -1
- package/dist/migrate.d.ts +30 -0
- package/dist/migrate.js +772 -0
- package/dist/migrate.js.map +1 -0
- package/dist/new-module.js +84 -43
- package/dist/new-module.js.map +1 -1
- package/dist/routes.d.ts +3 -5
- package/dist/routes.js +4 -6
- package/dist/routes.js.map +1 -1
- package/dist/spec.d.ts +2 -5
- package/dist/spec.js +13 -6
- package/dist/spec.js.map +1 -1
- package/dist/sync.d.ts +3 -11
- package/dist/sync.js +81 -20
- package/dist/sync.js.map +1 -1
- package/dist/template.d.ts +58 -0
- package/dist/template.js +244 -0
- package/dist/template.js.map +1 -0
- package/dist/update.js +4 -4
- package/package.json +17 -16
- package/templates/AGENTS.md +68 -31
- package/templates/CLAUDE.md +5 -4
- package/templates/app/.envs/.env.development +18 -0
- package/templates/app/.envs/.env.production +19 -0
- package/templates/app/.envs/.env.staging +19 -0
- package/templates/app/README.md +327 -0
- package/templates/app/app.ts +3 -0
- package/templates/app/config/app.config.ts +73 -0
- package/templates/app/docs/agent-manifest.json +724 -0
- package/templates/app/ecosystem.config.cjs +60 -0
- package/templates/app/modules/agent/controllers/agent.controllers.ts +31 -0
- package/templates/app/modules/agent/i18n/en.json +5 -0
- package/templates/app/modules/agent/i18n/fr.json +5 -0
- package/templates/app/modules/agent/module.acl.ts +14 -0
- package/templates/app/modules/agent/package.json +21 -0
- package/templates/app/modules/agent/routes/agent.routes.ts +29 -0
- package/templates/app/modules/agent/schemas/agent.schemas.ts +9 -0
- package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +26 -0
- package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +44 -0
- package/templates/app/modules/auth/controllers/auth.controllers.ts +312 -0
- package/templates/app/modules/auth/helpers/access-check.ts +42 -0
- package/templates/app/modules/auth/helpers/auth.ts +48 -0
- package/templates/app/modules/auth/helpers/documents.ts +57 -0
- package/templates/app/modules/auth/helpers/passport.ts +180 -0
- package/templates/app/modules/auth/helpers/passwords.ts +58 -0
- package/templates/app/modules/auth/helpers/session-cache.ts +67 -0
- package/templates/app/modules/auth/helpers/tokens.ts +329 -0
- package/templates/app/modules/auth/i18n/en.json +19 -0
- package/templates/app/modules/auth/i18n/fr.json +19 -0
- package/templates/app/modules/auth/lib/index.ts +12 -0
- package/templates/app/modules/auth/models/credential.model.ts +31 -0
- package/templates/app/modules/auth/models/revoked-token.model.ts +20 -0
- package/templates/app/modules/auth/models/session.model.ts +42 -0
- package/templates/app/modules/auth/module.acl.ts +41 -0
- package/templates/app/modules/auth/module.config.ts +30 -0
- package/templates/app/modules/auth/package.json +61 -0
- package/templates/app/modules/auth/routes/auth.routes.ts +94 -0
- package/templates/app/modules/auth/schemas/auth.schemas.ts +37 -0
- package/templates/app/modules/auth/tests/auth.test.ts +1079 -0
- package/templates/app/modules/config/controllers/config.controllers.ts +89 -0
- package/templates/app/modules/config/helpers/audit.ts +41 -0
- package/templates/app/modules/config/helpers/describe.ts +72 -0
- package/templates/app/modules/config/i18n/en.json +6 -0
- package/templates/app/modules/config/i18n/fr.json +6 -0
- package/templates/app/modules/config/models/config-audit.model.ts +26 -0
- package/templates/app/modules/config/module.acl.ts +12 -0
- package/templates/app/modules/config/package.json +23 -0
- package/templates/app/modules/config/routes/config.routes.ts +37 -0
- package/templates/app/modules/config/schemas/config.schemas.ts +13 -0
- package/templates/app/modules/config/tests/config.test.ts +126 -0
- package/templates/app/modules/core/bootstrap/cache.bootstrap.ts +12 -0
- package/templates/app/modules/core/helpers/cache/index.ts +60 -0
- package/templates/app/modules/core/helpers/cache/memory-cache.ts +53 -0
- package/templates/app/modules/core/helpers/cache/redis-cache.ts +68 -0
- package/templates/app/modules/core/helpers/cache/types.ts +37 -0
- package/templates/app/modules/core/helpers/lazy.ts +13 -0
- package/templates/app/modules/core/helpers/logger/index.ts +115 -0
- package/templates/app/modules/core/helpers/mongoose/index.ts +27 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/base.plugin.ts +14 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/soft-delete.plugin.ts +124 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/timestamps.plugin.ts +13 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/types.ts +45 -0
- package/templates/app/modules/core/helpers/mongoose/register.ts +20 -0
- package/templates/app/modules/core/lib/index.ts +34 -0
- package/templates/app/modules/core/module.config.ts +24 -0
- package/templates/app/modules/core/package.json +80 -0
- package/templates/app/modules/core/routes/core.routes.ts +24 -0
- package/templates/app/modules/core/tests/core.test.ts +19 -0
- package/templates/app/modules/hello/agent/hello.agent.ts +33 -0
- package/templates/app/modules/hello/bootstrap/seed-greetings.bootstrap.ts +27 -0
- package/templates/app/modules/hello/controllers/greetings.controllers.ts +43 -0
- package/templates/app/modules/hello/controllers/hello.controllers.ts +50 -0
- package/templates/app/modules/hello/helpers/greeting.ts +8 -0
- package/templates/app/modules/hello/i18n/en.json +8 -0
- package/templates/app/modules/hello/i18n/fr.json +8 -0
- package/templates/app/modules/hello/models/greeting.model.ts +24 -0
- package/templates/app/modules/hello/module.acl.ts +16 -0
- package/templates/app/modules/hello/module.config.ts +14 -0
- package/templates/app/modules/hello/package.json +24 -0
- package/templates/app/modules/hello/queues/hello.queues.ts +38 -0
- package/templates/app/modules/hello/routes/greetings.routes.ts +25 -0
- package/templates/app/modules/hello/routes/hello.routes.ts +44 -0
- package/templates/app/modules/hello/sockets/hello.sockets.ts +21 -0
- package/templates/app/modules/hello/tests/hello.test.ts +19 -0
- package/templates/app/modules/security/bootstrap/rate-limit.bootstrap.ts +8 -0
- package/templates/app/modules/security/helpers/hardening.ts +65 -0
- package/templates/app/modules/security/helpers/proxy.ts +16 -0
- package/templates/app/modules/security/helpers/rate-limit.ts +150 -0
- package/templates/app/modules/security/module.config.ts +59 -0
- package/templates/app/modules/security/package.json +26 -0
- package/templates/app/modules/security/routes/security.routes.ts +42 -0
- package/templates/app/modules/security/tests/security.test.ts +174 -0
- package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +12 -0
- package/templates/app/modules/settings/controllers/settings.controllers.ts +58 -0
- package/templates/app/modules/settings/helpers/access-lease.ts +99 -0
- package/templates/app/modules/settings/helpers/log-stream.ts +272 -0
- package/templates/app/modules/settings/helpers/log-tail.ts +253 -0
- package/templates/app/modules/settings/i18n/en.json +3 -0
- package/templates/app/modules/settings/i18n/fr.json +3 -0
- package/templates/app/modules/settings/module.acl.ts +12 -0
- package/templates/app/modules/settings/package.json +21 -0
- package/templates/app/modules/settings/routes/settings.routes.ts +33 -0
- package/templates/app/modules/settings/schemas/settings.schemas.ts +8 -0
- package/templates/app/modules/settings/tests/settings.test.ts +409 -0
- package/templates/app/modules/users/controllers/users.controllers.ts +113 -0
- package/templates/app/modules/users/i18n/en.json +14 -0
- package/templates/app/modules/users/i18n/fr.json +14 -0
- package/templates/app/modules/users/models/user.model.ts +18 -0
- package/templates/app/modules/users/module.acl.ts +10 -0
- package/templates/app/modules/users/module.config.ts +11 -0
- package/templates/app/modules/users/package.json +24 -0
- package/templates/app/modules/users/queues/users.queues.ts +31 -0
- package/templates/app/modules/users/routes/users.routes.ts +60 -0
- package/templates/app/modules/users/schemas/users.schemas.ts +30 -0
- package/templates/app/modules/users/sockets/users.sockets.ts +18 -0
- package/templates/app/nginx/development.conf +44 -0
- package/templates/app/nginx/production.conf +75 -0
- package/templates/app/nginx/staging.conf +51 -0
- package/templates/app/package.json +58 -0
- package/templates/app/scripts/count-greetings.ts +29 -0
- package/templates/app/scripts/deploy-nginx.ts +143 -0
- package/templates/app/scripts/list-users.ts +34 -0
- package/templates/app/tests/app.test.ts +28 -0
- package/templates/app/tests/support.ts +36 -0
- package/templates/app/tsconfig.json +28 -0
- package/templates/app/vitest.config.ts +6 -0
- package/templates/app/worker.ts +6 -0
- package/templates/claude/skills/tulipes-agent-tool/SKILL.md +83 -0
- package/templates/claude/skills/tulipes-boot-errors/SKILL.md +11 -5
- package/templates/claude/skills/tulipes-config/SKILL.md +74 -0
- package/templates/claude/skills/tulipes-endpoint/SKILL.md +38 -25
- package/templates/claude/skills/tulipes-env-variable/SKILL.md +54 -26
- package/templates/claude/skills/tulipes-model/SKILL.md +11 -4
- package/templates/claude/skills/tulipes-module/SKILL.md +11 -10
- package/templates/claude/skills/tulipes-queue/SKILL.md +20 -6
- package/templates/claude/skills/tulipes-settings/SKILL.md +49 -100
- package/templates/claude/skills/tulipes-spec/SKILL.md +10 -12
- package/templates/minimal/README.md +21 -0
- package/templates/minimal/app.ts +3 -0
- package/templates/minimal/config/app.config.ts +16 -0
- package/templates/minimal/modules/agent/module.acl.ts +3 -0
- package/templates/minimal/modules/agent/package.json +14 -0
- package/templates/minimal/modules/agent/routes/agent.routes.ts +20 -0
- package/templates/minimal/modules/core/module.acl.ts +4 -0
- package/templates/minimal/modules/core/package.json +21 -0
- package/templates/minimal/modules/hello/i18n/en.json +3 -0
- package/templates/minimal/modules/hello/module.acl.ts +2 -0
- package/templates/minimal/modules/hello/package.json +11 -0
- package/templates/minimal/modules/hello/routes/health.routes.ts +9 -0
- package/templates/minimal/modules/hello/routes/hello.routes.ts +10 -0
- package/templates/minimal/modules/security/package.json +15 -0
- package/templates/minimal/modules/security/routes/security.routes.ts +8 -0
- package/templates/minimal/package.json +38 -0
- package/templates/minimal/tests/app.test.ts +52 -0
- package/templates/minimal/tsconfig.json +22 -0
- package/templates/minimal/vitest.config.ts +5 -0
- package/templates/overlays/no-queues/ecosystem.config.cjs +52 -0
- package/templates/overlays/no-queues/modules/hello/controllers/greetings.controllers.ts +41 -0
- package/templates/overlays/no-queues/modules/users/controllers/users.controllers.ts +107 -0
- package/templates/versions.json +11 -0
- package/dist/minimal.d.ts +0 -2
- package/dist/minimal.js +0 -2200
- package/dist/minimal.js.map +0 -1
- /package/templates/{browser-auth.md → app/docs/browser-auth.md} +0 -0
package/templates/AGENTS.md
CHANGED
|
@@ -16,22 +16,32 @@ one contract in depth.
|
|
|
16
16
|
## Preset scope
|
|
17
17
|
|
|
18
18
|
Read the installed modules and package scripts before applying integration examples.
|
|
19
|
-
The minimal preset has core, security and
|
|
20
|
-
MongoDB, Redis, queues, sockets, shared logger/cache helpers
|
|
21
|
-
below apply when those modules are installed (the production
|
|
19
|
+
The minimal preset has core, security, hello and agent only. Auth, users,
|
|
20
|
+
config, settings, MongoDB, Redis, queues, sockets, shared logger/cache helpers
|
|
21
|
+
and PM2 examples below apply when those modules are installed (the production
|
|
22
|
+
preset). Minimal
|
|
22
23
|
requests use the framework guest identity. Grant permissions to roles the app
|
|
23
24
|
actually defines. Use the generated Vitest suite for changes; production tests
|
|
24
25
|
require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
25
26
|
|
|
26
27
|
## Non-negotiable rules
|
|
27
28
|
|
|
28
|
-
1. **Never read `process.env` directly
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
1. **Never read `process.env` directly, and keep the environment small.**
|
|
30
|
+
A value is an environment variable only if boot needs it before the
|
|
31
|
+
database answers (connection strings, the port, log destination) or it
|
|
32
|
+
is a secret: declared under `tulipes.env` in the module's `package.json`, read with
|
|
33
|
+
`ctx.Environment.get("NAME")`. Everything else is a *setting*: declared
|
|
34
|
+
with a zod schema in that module's `module.config.ts`, stored in the
|
|
35
|
+
settings store (the `settings` collection here; a JSON file under
|
|
36
|
+
`config/` in an app without the Mongo store), edited through the
|
|
37
|
+
console, read live at `ctx.config.<module>`. → skill `tulipes-env-variable`
|
|
32
38
|
2. **A feature is a module, not a layer.** Everything a feature needs —
|
|
33
39
|
routes, models, config, permissions, jobs — lives in one folder under
|
|
34
40
|
`modules/`. Never add a top-level `services/` or `middlewares/` folder.
|
|
41
|
+
A module's root holds exactly `package.json`, `module.config.ts` and
|
|
42
|
+
`module.acl.ts`; everything else has a folder (`routes/`, `controllers/`,
|
|
43
|
+
`models/`, `schemas/`, `helpers/`, `bootstrap/`, `queues/`, `sockets/`,
|
|
44
|
+
`i18n/`, `agent/`, `lib/`, `tests/`), and the boot refuses a stray root file.
|
|
35
45
|
→ skill `tulipes-module`
|
|
36
46
|
3. **Never call `mongoose.model()` yourself.** Models are declarations
|
|
37
47
|
picked up by the framework and read from `ctx.models`. Persisted and
|
|
@@ -39,16 +49,23 @@ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
|
39
49
|
camelCase is for TypeScript-only names. → skill `tulipes-model`
|
|
40
50
|
4. **Global middleware belongs to a sys-tier module**, not to the
|
|
41
51
|
entrypoint. `app.ts` stays three lines. → skill `tulipes-endpoint`
|
|
42
|
-
5. **Run `
|
|
43
|
-
`module.config.ts
|
|
44
|
-
|
|
52
|
+
5. **Run `pnpm sync` after touching a module's `tulipes.env`, a
|
|
53
|
+
settings schema in `module.config.ts`, a model file or a queue file's
|
|
54
|
+
`jobs` export.** It regenerates `types/tulipes.d.ts` and `.env.example`;
|
|
55
|
+
stale generated types are a lie. With it, `ctx.models.get("User")` is
|
|
56
|
+
typed from the model's schema and `queues.add("users.welcome", …)` from
|
|
57
|
+
the queue's job schema, so a wrong name or payload does not compile. `module.config.ts`
|
|
58
|
+
has exactly one export, `defineConfig({ schema })`; initialization and
|
|
59
|
+
cleanup belong to `bootstrap/*.bootstrap.ts`, which may return the
|
|
60
|
+
function that closes what it opened, and long-lived work ends on
|
|
61
|
+
`ctx.signal`.
|
|
45
62
|
6. **Every response is `res.respond({ data, meta })`** — never `res.json`,
|
|
46
63
|
which ships a raw body outside the envelope. Failures are **thrown**:
|
|
47
64
|
`throw new HttpError(404, "notFound")`, where the message is an i18n
|
|
48
65
|
key and the code derives from it. Never hand-roll an error shape.
|
|
49
66
|
→ skill `tulipes-response`
|
|
50
67
|
7. **Every route declares a RAI**: put `rai({ id: "x:read" })` first in
|
|
51
|
-
each endpoint handler chain, including routes
|
|
68
|
+
each endpoint handler chain, including routes on a nested router.
|
|
52
69
|
The id is also the ACL permission, and core refuses to boot if a route
|
|
53
70
|
is missing one. Public is `acl.allow("guest", "x:read")`, never a
|
|
54
71
|
missing check. → skill `tulipes-endpoint`
|
|
@@ -68,30 +85,47 @@ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
|
68
85
|
the auth module's `Credential` model, which is `select: false`.
|
|
69
86
|
12. **When auth is installed, a new feature module's namespace goes in BOTH the `admin` and
|
|
70
87
|
`sysadmin` grants** in `modules/auth/module.acl.ts`. Neither role holds
|
|
71
|
-
`*` — a wildcard would reach `settings:*` and make
|
|
72
|
-
boundary a label rather than a rule. →
|
|
88
|
+
`*` — a wildcard would reach `config:write` and `settings:*` and make
|
|
89
|
+
the sysadmin boundary a label rather than a rule. → skills
|
|
90
|
+
`tulipes-config`, `tulipes-settings`
|
|
73
91
|
13. **Cross-module imports use the package name** (`@app/users`,
|
|
74
92
|
`@app/core`), never a relative path that climbs out of the module. The
|
|
75
93
|
workspace boundary is what keeps modules independent, and a package
|
|
76
94
|
name makes the dependency visible in package.json — declare it there
|
|
77
95
|
and in `dependsOn`.
|
|
96
|
+
14. **Every endpoint is a tool an agent can call; describe it as one.** The
|
|
97
|
+
`rai()` name, description and schemas are what the manifest shows, so a
|
|
98
|
+
bare `id` is a tool with no description. A module's `agent/*.agent.ts`
|
|
99
|
+
adds a summary, curates the routes and declares actions that are not
|
|
100
|
+
endpoints; `agent: false` on a `rai()` hides one. → skill
|
|
101
|
+
`tulipes-agent-tool`
|
|
102
|
+
15. **A module's tests live in `modules/<name>/tests/` and boot the app
|
|
103
|
+
through `@tulipes/testing`**: `useTestApp({ rootDir }, { beforeAll,
|
|
104
|
+
afterAll })` once per file, `app.get(app.api("/x"))` with the envelope
|
|
105
|
+
decoded, `app.as("admin")` for a signed-in role. Never boot per test, and
|
|
106
|
+
never reach into another module's files from a test.
|
|
78
107
|
|
|
79
108
|
For endpoint changes, read `tulipes-endpoint`: native routers live in
|
|
80
|
-
`routes/*.routes.ts
|
|
81
|
-
|
|
109
|
+
`routes/*.routes.ts`, plain `(ctx) => Router` functions: build the router from
|
|
110
|
+
the context, return it for the configured prefix, and read models, settings
|
|
111
|
+
and the environment inside handlers, never while building — `tulipes routes`
|
|
112
|
+
and `tulipes spec` build every router with nothing acquired. Global
|
|
113
|
+
middleware that needs the environment is built on its first request with
|
|
114
|
+
`lazy()` from `@app/core`.
|
|
82
115
|
|
|
83
116
|
## Where things go
|
|
84
117
|
|
|
85
118
|
| Decision | Location |
|
|
86
119
|
|---|---|
|
|
87
|
-
|
|
|
88
|
-
| Policy shared by ≥2 modules (pagination,
|
|
89
|
-
|
|
|
120
|
+
| Connection string, secret, or anything boot needs before the database answers | `tulipes.env` in that module's `package.json` |
|
|
121
|
+
| Policy shared by ≥2 modules and the same everywhere (pagination, locales) | `config/app.config.ts` |
|
|
122
|
+
| A value an operator may change at runtime (limits, modes, toggles, lifetimes) | a field in that module's `module.config.ts` schema, read at `ctx.config.<module>` |
|
|
123
|
+
| Opening a client, seeding rows, ensuring indexes, and closing them again | that module's `bootstrap/*.bootstrap.ts`, returning the cleanup |
|
|
90
124
|
| A string a user will read | that module's `i18n/<locale>.json`, reached with `req.t` |
|
|
91
125
|
| The shape of a request body/query | a zod schema on the route's `rai()` |
|
|
92
126
|
| Middleware for every request | a sys-tier module's routes file |
|
|
93
127
|
| Middleware for one feature's routes | that module's own `Router` |
|
|
94
|
-
| Helper used by ≥2 modules | the sys core module, exported from its `index.ts` and imported as `@app/core` |
|
|
128
|
+
| Helper used by ≥2 modules | the sys core module, exported from its `lib/index.ts` and imported as `@app/core` |
|
|
95
129
|
| Helper used by one module | that module's `helpers/` |
|
|
96
130
|
| One-off task (backfill, export, admin chore) | `scripts/`, booted with `mode: "script"` |
|
|
97
131
|
| Anything you would `console.log` | `moduleLogger(ctx.Environment, "<module>", ctx.mode)` from `@app/core` |
|
|
@@ -99,27 +133,30 @@ keep shared middleware and param callbacks beside their endpoints.
|
|
|
99
133
|
## Boot order (why load order matters)
|
|
100
134
|
|
|
101
135
|
`sys` modules load before `app` modules; within a tier, lower `priority`
|
|
102
|
-
first; then `dependsOn` topological order.
|
|
103
|
-
|
|
104
|
-
provides, declare `dependsOn` — do not rely on
|
|
136
|
+
first; then `dependsOn` topological order. Declarations → models →
|
|
137
|
+
settings → translations → ACL → bootstrap → queues → routes. If code needs
|
|
138
|
+
something another module provides, declare `dependsOn` — do not rely on
|
|
139
|
+
luck.
|
|
105
140
|
|
|
106
141
|
## Commands
|
|
107
142
|
|
|
108
143
|
| Command | Use |
|
|
109
144
|
|---|---|
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
145
|
+
| `pnpm dev` | run the backend process (regenerates types first) |
|
|
146
|
+
| `pnpm worker` | run the queue-consumer process |
|
|
147
|
+
| `pnpm sync` | regenerate `types/tulipes.d.ts` + `.env.example` |
|
|
148
|
+
| `pnpm tulipes agent manifest [--role r]` | print every tool the app exposes to agents, offline |
|
|
149
|
+
| `pnpm test` | every `tests/` and `modules/*/tests/` file; each boots the app once through `@tulipes/testing` |
|
|
150
|
+
| `pnpm check` | validate the environment without booting |
|
|
151
|
+
| `pnpm typecheck` | typecheck the whole app |
|
|
152
|
+
| `pnpm tulipes new module <name>` | scaffold a module |
|
|
116
153
|
| `tulipes update` | upgrade the global cli and the framework in every manifest |
|
|
117
|
-
| `
|
|
154
|
+
| `pnpm script scripts/x.ts` | run a one-off script against the real app |
|
|
118
155
|
|
|
119
156
|
## Verifying your work
|
|
120
157
|
|
|
121
|
-
A change is not done until the app boots. Run `
|
|
122
|
-
`
|
|
158
|
+
A change is not done until the app boots. Run `pnpm typecheck`, then
|
|
159
|
+
`pnpm dev` and confirm the startup banner lists what you added (modules,
|
|
123
160
|
queues, sockets, roles). Boot failures print an aggregate report naming
|
|
124
161
|
every problem at once — read all of it before fixing anything. → skill
|
|
125
162
|
`tulipes-boot-errors`
|
package/templates/CLAUDE.md
CHANGED
|
@@ -6,7 +6,8 @@ here; it holds the non-negotiable rules, where each kind of code belongs, the
|
|
|
6
6
|
boot order and the commands.
|
|
7
7
|
|
|
8
8
|
This file exists so Claude Code picks that up automatically. The other
|
|
9
|
-
Claude-specific piece is `.claude/skills/`:
|
|
10
|
-
environment variable, endpoint, model, queue, socket, permissions
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Claude-specific piece is `.claude/skills/`: task-scoped guides — module,
|
|
10
|
+
environment variable, endpoint, response, model, queue, socket, permissions,
|
|
11
|
+
settings (the config module and the log stream), i18n, spec, agent tools and
|
|
12
|
+
reading boot errors — loaded on demand when a task matches one. AGENTS.md
|
|
13
|
+
points at them by name.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Development. NOT committed — .gitignore excludes .envs/, because a
|
|
2
|
+
# file holding working credentials does not belong in a repository.
|
|
3
|
+
# The committed contract is .env.example at the root: copy it here and
|
|
4
|
+
# fill it in. Real deployments inject variables through the container
|
|
5
|
+
# environment, which always wins over this file.
|
|
6
|
+
|
|
7
|
+
PORT=3000
|
|
8
|
+
LOG_LEVEL=debug
|
|
9
|
+
|
|
10
|
+
MONGO_URI=mongodb://127.0.0.1:27017/tulipes-example
|
|
11
|
+
REDIS_URL=redis://127.0.0.1:6379
|
|
12
|
+
|
|
13
|
+
# Auth. Separate keys, so leaking one cannot mint the other.
|
|
14
|
+
JWT_ACCESS_SECRET=dev-only-access-secret-change-me
|
|
15
|
+
JWT_REFRESH_SECRET=dev-only-refresh-secret-change-me
|
|
16
|
+
AUTH_ADMIN_EMAIL=admin@example.com
|
|
17
|
+
AUTH_ADMIN_PASSWORD=dev-admin-password
|
|
18
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Production. NOT committed — .gitignore excludes .envs/. Recreate it
|
|
2
|
+
# on the deployment host from .env.example, or leave it out entirely
|
|
3
|
+
# and inject everything through the container environment.
|
|
4
|
+
#
|
|
5
|
+
# The blanks below are intentional. Real deployments inject MONGO_URI,
|
|
6
|
+
# REDIS_URL and any secret through the container environment, which
|
|
7
|
+
# always wins over this file. Booting without them fails closed with a
|
|
8
|
+
# report naming every missing variable — run `pnpm check` to see it
|
|
9
|
+
# without starting the app.
|
|
10
|
+
|
|
11
|
+
PORT=3000
|
|
12
|
+
LOG_LEVEL=info
|
|
13
|
+
|
|
14
|
+
# MONGO_URI=
|
|
15
|
+
# REDIS_URL=
|
|
16
|
+
# JWT_ACCESS_SECRET=
|
|
17
|
+
# JWT_REFRESH_SECRET=
|
|
18
|
+
# AUTH_ADMIN_EMAIL=
|
|
19
|
+
# AUTH_ADMIN_PASSWORD=
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Staging. NOT committed — .gitignore excludes .envs/. Recreate it on
|
|
2
|
+
# the deployment host from .env.example, or leave it out entirely and
|
|
3
|
+
# inject everything through the container environment.
|
|
4
|
+
#
|
|
5
|
+
# The blanks below are intentional. Real deployments inject MONGO_URI,
|
|
6
|
+
# REDIS_URL and any secret through the container environment, which
|
|
7
|
+
# always wins over this file. Booting without them fails closed with a
|
|
8
|
+
# report naming every missing variable — run `pnpm check` to see it
|
|
9
|
+
# without starting the app.
|
|
10
|
+
|
|
11
|
+
PORT=3000
|
|
12
|
+
LOG_LEVEL=info
|
|
13
|
+
|
|
14
|
+
# MONGO_URI=
|
|
15
|
+
# REDIS_URL=
|
|
16
|
+
# JWT_ACCESS_SECRET=
|
|
17
|
+
# JWT_REFRESH_SECRET=
|
|
18
|
+
# AUTH_ADMIN_EMAIL=
|
|
19
|
+
# AUTH_ADMIN_PASSWORD=
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# tulipes-example
|
|
2
|
+
|
|
3
|
+
A production-preset [Tulipes](https://www.npmjs.com/package/@tulipes/core) application. Requires Node 24.x.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g pnpm # once per machine
|
|
9
|
+
pnpm install
|
|
10
|
+
pnpm dev # boot the backend process → http://localhost:3000/api/v1/hello
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Needs mongo and redis running — see `MONGO_URI` and `REDIS_URL` in
|
|
14
|
+
`.envs/.env.development`. Run `pnpm worker` in a second terminal to
|
|
15
|
+
consume queued jobs.
|
|
16
|
+
|
|
17
|
+
`.envs/` is git-ignored: those files hold working credentials. On a
|
|
18
|
+
fresh clone there is nothing in it, so copy `.env.example` — which is
|
|
19
|
+
committed, and which `pnpm sync` regenerates from every module's
|
|
20
|
+
declared variables — and fill it in:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pnpm sync # refresh .env.example and types
|
|
24
|
+
mkdir -p .envs # git does not track empty dirs
|
|
25
|
+
cp .env.example .envs/.env.development
|
|
26
|
+
pnpm check # validate it without booting
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Verify
|
|
30
|
+
|
|
31
|
+
Run `pnpm sync`, `pnpm typecheck` and `pnpm check`. Tests need dedicated
|
|
32
|
+
services: `TEST_MONGO_URI=mongodb://127.0.0.1:27017/tulipes_tests TEST_REDIS_URL=redis://127.0.0.1:6379/3 pnpm test`.
|
|
33
|
+
Use test-only data: the suite creates users and sessions. `pnpm spec` writes
|
|
34
|
+
OpenAPI/Postman documents offline, without services or runtime secrets.
|
|
35
|
+
Change the shared API prefix in `config/routes.ts`. Runtime middleware stays
|
|
36
|
+
with endpoints in `routes/*.routes.ts`, plain functions of the context.
|
|
37
|
+
Staging/production boot rejects development credentials and equal JWT keys.
|
|
38
|
+
Auth, settings and deployment examples are application policy you own.
|
|
39
|
+
|
|
40
|
+
## The hello module
|
|
41
|
+
|
|
42
|
+
`modules/hello` is a worked example of every module contract — read it
|
|
43
|
+
before writing your own:
|
|
44
|
+
|
|
45
|
+
| File | Contract |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `package.json` `tulipes.env` | the module's environment contract |
|
|
48
|
+
| `module.config.ts` | `defineConfig({ schema })`: this module's settings |
|
|
49
|
+
| `module.acl.ts` | permission grants on `hello:*` |
|
|
50
|
+
| `routes/*.routes.ts` | native routers, shared middleware and RAI schemas |
|
|
51
|
+
| `controllers/` | the work; constructed from the context while routes build |
|
|
52
|
+
| `helpers/` | pure logic, no framework |
|
|
53
|
+
| `i18n/<locale>.json` | this module's strings — the module is the namespace |
|
|
54
|
+
| `sockets/*.sockets.ts` | a claimed Socket.IO namespace | <!-- feature:sockets -->
|
|
55
|
+
| `models/*.model.ts` | a schema declaration for the model store |
|
|
56
|
+
| `bootstrap/*.bootstrap.ts` | idempotent seeding, before traffic |
|
|
57
|
+
| `queues/*.queues.ts` | a job definition and its processor | <!-- feature:queues -->
|
|
58
|
+
|
|
59
|
+
## Daily commands
|
|
60
|
+
|
|
61
|
+
| command | does |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `pnpm dev` | regenerate types, run the backend process with `node --watch` |
|
|
64
|
+
| `pnpm worker` | run the queue-consumer process (needs a queue first) | <!-- feature:queues -->
|
|
65
|
+
| `pnpm sync` | regenerate `types/tulipes.d.ts` + `.env.example` |
|
|
66
|
+
| `pnpm check` | validate env against every module contract — CI gate |
|
|
67
|
+
| `pnpm tulipes new module <name>` | scaffold a module |
|
|
68
|
+
| `pnpm tulipes update` | upgrade the framework everywhere it is declared |
|
|
69
|
+
|
|
70
|
+
## Authentication
|
|
71
|
+
|
|
72
|
+
`modules/auth` is sys tier, so it resolves the caller before any
|
|
73
|
+
feature route runs. A request with no valid token is not rejected — it
|
|
74
|
+
becomes the **guest** caller, which is what makes a public endpoint
|
|
75
|
+
an ordinary ACL grant instead of a second mechanism:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
acl.allow("guest", "auth:login"); // that is why login is reachable
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Guarding is the RAI itself — its `id` IS the permission, so there is
|
|
82
|
+
no second call to forget. 401 means "sign in"; 403 means "signed in,
|
|
83
|
+
still not allowed":
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
router.get(
|
|
87
|
+
`${base}/invoices`,
|
|
88
|
+
rai({ id: "invoices:read", name: "List invoices" }),
|
|
89
|
+
billing.list(),
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A route that declares no RAI would be reachable by guest, so core
|
|
94
|
+
scans the router stack after mounting and refuses to boot when it
|
|
95
|
+
finds one. Run `pnpm tulipes routes` to see every endpoint with the
|
|
96
|
+
roles that reach it.
|
|
97
|
+
|
|
98
|
+
Try it — the admin comes from AUTH_ADMIN_EMAIL / AUTH_ADMIN_PASSWORD:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
curl -X POST localhost:3000/api/v1/auth/login \
|
|
102
|
+
-H 'content-type: application/json' \
|
|
103
|
+
-d '{"email":"admin@example.com","password":"dev-admin-password"}'
|
|
104
|
+
|
|
105
|
+
curl localhost:3000/api/v1/auth/me -H "authorization: Bearer <accessToken>"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Inside a handler, the caller is `req.auth` — resolved before any
|
|
109
|
+
feature route and never undefined inside a declared one:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const { isAuthenticated, user, session } = req.auth;
|
|
113
|
+
user?.email // the hydrated user document, null for a guest
|
|
114
|
+
session?.sid // this device's session row, null for a guest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Never re-parse the Authorization header: the pipeline already did. It
|
|
118
|
+
resolves the session and the user on every request — from the app cache
|
|
119
|
+
(Redis, shared by every replica) when a request resolved them recently,
|
|
120
|
+
from the database otherwise — and every path that revokes (logout,
|
|
121
|
+
refresh reuse, password change, a session ended, a deleted account)
|
|
122
|
+
forgets the cached entry first, so a demotion, a ban or a logout takes
|
|
123
|
+
effect on the very next request on any replica. Code that writes a
|
|
124
|
+
user's role calls `forgetUser()` from `@app/auth` for the same reason.
|
|
125
|
+
|
|
126
|
+
Sessions are rows, so a signed-in account can see and end its own
|
|
127
|
+
devices — `GET /auth/sessions`, `DELETE /auth/sessions/:sid`. Ending
|
|
128
|
+
one takes effect on the next request that device makes, even though
|
|
129
|
+
its access token is still signed and unexpired.
|
|
130
|
+
|
|
131
|
+
Passwords never touch the User document: the hash lives in the auth
|
|
132
|
+
module's `Credential` model, `select: false`, so a route that
|
|
133
|
+
serialises a user cannot leak one. Refresh tokens rotate on use;
|
|
134
|
+
logout deletes the refresh row and blacklists the access token's jti
|
|
135
|
+
until it expires; a password change moves a per-user epoch that
|
|
136
|
+
invalidates every token issued before it. Both token collections carry
|
|
137
|
+
TTL indexes.
|
|
138
|
+
|
|
139
|
+
## The console
|
|
140
|
+
|
|
141
|
+
`modules/config` is the operator's console: `GET /api/v1/config`
|
|
142
|
+
describes every module — manifest, declared variables (names, types and
|
|
143
|
+
sources; a secret's value never), settings with their schema, live values
|
|
144
|
+
and stored version, endpoints with the roles that reach them — and
|
|
145
|
+
`PATCH /api/v1/config/:module` changes a module's settings: validated by
|
|
146
|
+
its schema, refused on a stale version, audited (names, never values)
|
|
147
|
+
before it lands, applied at once in every replica. Reading needs `admin`;
|
|
148
|
+
writing needs `sysadmin`. Settings live in MongoDB (`settings` and
|
|
149
|
+
`settings_history`) because the root `package.json` declares
|
|
150
|
+
`@tulipes/mongoose/settings`; the request pipeline — proxy trust, CORS,
|
|
151
|
+
HSTS, the body limit, the rate limits — is configured from
|
|
152
|
+
`config.security` this way, so none of it is an environment variable.
|
|
153
|
+
|
|
154
|
+
## Logging
|
|
155
|
+
|
|
156
|
+
The logger lives in the core module and is shared by every other one:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { moduleLogger } from "@app/core";
|
|
160
|
+
|
|
161
|
+
const log = moduleLogger(ctx.Environment, "billing", ctx.mode);
|
|
162
|
+
log.info({ invoiceId }, "invoice sent");
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
It writes to two destinations at once, because they answer different
|
|
166
|
+
questions: a **coloured, human-readable** stream on the console, and
|
|
167
|
+
**JSON lines** in `logs/` — one object per line, which is what makes
|
|
168
|
+
them greppable with jq or ingestible by a collector. Errors are
|
|
169
|
+
additionally copied to their own file, so the first thing to open when
|
|
170
|
+
something breaks is small.
|
|
171
|
+
|
|
172
|
+
Files carry the process mode — `app-backend.log`, `app-worker.log` —
|
|
173
|
+
because backend and worker run at the same time and two processes
|
|
174
|
+
rotating one file will race.
|
|
175
|
+
|
|
176
|
+
The destinations are environment, because the logger exists before the
|
|
177
|
+
settings store answers: `LOG_LEVEL`, `LOG_CONSOLE`, `LOG_FILE` and
|
|
178
|
+
`LOG_DIR`, declared in `modules/core/package.json`. The file names and
|
|
179
|
+
the rotation (daily or 20m, whichever trips first; fourteen rotated
|
|
180
|
+
files kept) never needed to differ per deployment, so they are the
|
|
181
|
+
`LOG_FILES` constants in `modules/core/helpers/logger/`. A `sysadmin`
|
|
182
|
+
can watch the files live: `GET /api/v1/settings/logs/stream` (SSE).
|
|
183
|
+
|
|
184
|
+
## Responses
|
|
185
|
+
|
|
186
|
+
Every endpoint answers with one envelope, so a client unwraps once:
|
|
187
|
+
|
|
188
|
+
```jsonc
|
|
189
|
+
{ "success": true, "data": { }, "errors": [], "meta": {} }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`res.respond` builds it. Express is untouched — `res.json` and
|
|
193
|
+
`res.status` behave exactly as they always did:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
res.respond({ data: user });
|
|
197
|
+
res.respond({ data: user, status: 201 });
|
|
198
|
+
res.respond({ data: items, meta: { total_items } }); // paginated
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Failures are **thrown**, never responded — the framework's terminal
|
|
202
|
+
handler builds the same envelope from them, so a handler only writes
|
|
203
|
+
the happy path. The message is an i18n key: it gets translated in the
|
|
204
|
+
module's namespace and the error code is derived from it.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
throw new HttpError(404, "notFound"); // → NOT_FOUND
|
|
208
|
+
throw new HttpError(422, "emailTaken", { field: "email" });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`req.page` is already parsed from `?page` / `?per_page` and clamped
|
|
212
|
+
by `config.pagination`; passing `total_items` is what marks a
|
|
213
|
+
response as a page and fills in `total_pages`.
|
|
214
|
+
|
|
215
|
+
In development, a route answering without `res.respond` logs a
|
|
216
|
+
warning naming it.
|
|
217
|
+
|
|
218
|
+
## Browser authentication
|
|
219
|
+
|
|
220
|
+
Read [Browser authentication and cookies](docs/browser-auth.md) before wiring
|
|
221
|
+
up a browser client. The starter uses Bearer headers and JSON refresh tokens;
|
|
222
|
+
CORS settings do not install cookie authentication or CSRF protection.
|
|
223
|
+
|
|
224
|
+
## Translations
|
|
225
|
+
|
|
226
|
+
Each module owns its strings and IS its own i18n namespace:
|
|
227
|
+
`modules/hello/i18n/en.json`, `fr.json`. Inside an endpoint `req.t`
|
|
228
|
+
is already bound to the module that declared the route — `rai()` binds
|
|
229
|
+
it — so there is nothing to import and no namespace to name:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
res.json({ message: req.t("created", { email }) });
|
|
233
|
+
throw new HttpError(404, req.t("notFound"));
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The locale is `?lang=fr`, else Accept-Language, else
|
|
237
|
+
`config.i18n.defaultLocale` — narrowed to `supportedLocales`:
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
curl 'localhost:3000/api/v1/hello/secret?lang=fr' -H "authorization: Bearer <token>"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Code with no request names both: `ctx.t("users", recipient.locale)`,
|
|
244
|
+
which is how a queue processor translates against the recipient's
|
|
245
|
+
locale rather than the enqueuer's — so put the locale in the job.
|
|
246
|
+
|
|
247
|
+
Only the default locale must be complete. Anything another locale
|
|
248
|
+
lacks falls back to it and is listed in `<locale>.missing.json` beside
|
|
249
|
+
it on the next boot — git-ignored, and never written in production.
|
|
250
|
+
|
|
251
|
+
## Scripts
|
|
252
|
+
|
|
253
|
+
`scripts/` holds one-off tasks — backfills, exports, admin chores.
|
|
254
|
+
They boot the app in **script mode**, which runs every phase the
|
|
255
|
+
backend runs and then starts no server, so a script gets exactly what
|
|
256
|
+
a route handler gets: config, ACL, models, queue producers.
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
pnpm script scripts/count-greetings.ts
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Copy the try/finally from `scripts/count-greetings.ts`: mongo and
|
|
263
|
+
redis hold the event loop open, so a script that forgets
|
|
264
|
+
`handle.shutdown()` prints its output and then hangs.
|
|
265
|
+
|
|
266
|
+
## Deploying nginx
|
|
267
|
+
|
|
268
|
+
`nginx/{development,staging,production}.conf` are templates with
|
|
269
|
+
`{{DOMAIN}}` and `{{PORT}}` placeholders. The deploy script renders
|
|
270
|
+
the one matching `APP_ENV`, using that same environment's
|
|
271
|
+
the `core.publicDomain` setting and `PORT`, so template, domain and port can never
|
|
272
|
+
disagree:
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
APP_ENV=production pnpm script scripts/deploy-nginx.ts --dry-run
|
|
276
|
+
sudo -E APP_ENV=production pnpm script scripts/deploy-nginx.ts
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
It writes `sites-available/<domain>`, links `sites-enabled`, runs
|
|
280
|
+
`nginx -t`, then reloads. It refuses when not root, while
|
|
281
|
+
`core.publicDomain` is still `localhost`, or when the conf already exists
|
|
282
|
+
(pass `--force` to overwrite one). Debian/Ubuntu layout.
|
|
283
|
+
|
|
284
|
+
The rendered conf is HTTP-only with the TLS block commented out —
|
|
285
|
+
`nginx -t` fails on certificate paths that do not exist yet, which
|
|
286
|
+
would make the first deploy impossible. Install the certificate,
|
|
287
|
+
uncomment the block, redeploy with `--force`.
|
|
288
|
+
|
|
289
|
+
## Running under PM2
|
|
290
|
+
|
|
291
|
+
`ecosystem.config.cjs` defines both processes — the backend and the
|
|
292
|
+
queue worker — in all three modes:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
pnpm pm2:start # development
|
|
296
|
+
pnpm pm2:start:staging
|
|
297
|
+
pnpm pm2:start:production
|
|
298
|
+
|
|
299
|
+
pnpm pm2:status # what is running
|
|
300
|
+
pnpm pm2:logs # tail both processes
|
|
301
|
+
pnpm pm2:restart # reload code
|
|
302
|
+
pnpm pm2:stop # stop, keep them in the process list
|
|
303
|
+
pnpm pm2:delete # remove from the process list
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Switching environment requires a restart carrying the flag, because
|
|
307
|
+
PM2 remembers the environment a process was started with:
|
|
308
|
+
`pm2 restart ecosystem.config.cjs --env production`.
|
|
309
|
+
|
|
310
|
+
`.envs/` holds one file per mode and is **git-ignored** — those files
|
|
311
|
+
carry working credentials. What is committed is `.env.example`,
|
|
312
|
+
generated by `pnpm sync`, naming every variable any module declares:
|
|
313
|
+
a clone copies it into `.envs/.env.<mode>` and fills it in. Servers
|
|
314
|
+
usually skip the file entirely and inject variables through the
|
|
315
|
+
container environment, which always wins over it. `pnpm check`
|
|
316
|
+
validates the current environment without booting, so a deploy can
|
|
317
|
+
fail on configuration before it fails on traffic.
|
|
318
|
+
|
|
319
|
+
PM2 is a devDependency so the scripts work straight after install;
|
|
320
|
+
servers usually install it globally instead (`npm i -g pm2`), in
|
|
321
|
+
which case the same scripts still work.
|
|
322
|
+
|
|
323
|
+
Two defaults worth knowing: processes run in **fork** mode with a
|
|
324
|
+
single instance, because Socket.IO needs sticky sessions or the Redis
|
|
325
|
+
adapter before a second instance is safe; and `kill_timeout` is 10s so
|
|
326
|
+
graceful shutdown can drain queues and close mongo and redis before
|
|
327
|
+
PM2 sends SIGKILL.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AppConfigFn } from "@tulipes/core/boot";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ── The app-wide config seed ──────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Runs FIRST in boot phase 6, before any module.config.ts factory, and
|
|
7
|
+
* whatever it returns becomes the base of the global config. Modules read
|
|
8
|
+
* it through `ctx.config.<key>`.
|
|
9
|
+
*
|
|
10
|
+
* Add a key here when two or more modules share a value. Config belonging
|
|
11
|
+
* to ONE feature goes in that module's `module.config.ts` instead, where
|
|
12
|
+
* `pnpm sync` types it for you — that is where `users.maxSessions` and
|
|
13
|
+
* `users.signupMode` live. A value that differs per deployment belongs in
|
|
14
|
+
* neither: declare it in a module's `meta.variables.json` and read it with
|
|
15
|
+
* `Environment.get`, the way MONGO_URI, CORS_ORIGINS and BODY_LIMIT do.
|
|
16
|
+
*
|
|
17
|
+
* The framework reads exactly one field of this object: `app.name`, which
|
|
18
|
+
* titles the startup banner. Everything else is yours.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// `satisfies` (not a type annotation) so TypeScript keeps the exact shape
|
|
22
|
+
// of this object for the augmentation at the bottom of the file.
|
|
23
|
+
const appConfig = (() => ({
|
|
24
|
+
app: {
|
|
25
|
+
name: "tulipes-example",
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Route prefixing. A router a routes file returns is mounted under this
|
|
30
|
+
* prefix, so versioning the whole API is a one-line change here; a module
|
|
31
|
+
* that wants another path says so with `ctx.routes.mount(router, "/x")`.
|
|
32
|
+
*/
|
|
33
|
+
api: {
|
|
34
|
+
prefix: "/api/v1",
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The languages this app speaks. Every module owns its own strings in
|
|
39
|
+
* `modules/<name>/i18n/<locale>.json` — the module is the namespace —
|
|
40
|
+
* and `req.t` inside an endpoint is already bound to the module that
|
|
41
|
+
* declared it. A request picks its locale with `?lang=fr` or an
|
|
42
|
+
* Accept-Language header, narrowed to this list.
|
|
43
|
+
*
|
|
44
|
+
* `defaultLocale` is also the fallback: a key missing from fr.json is
|
|
45
|
+
* served from en.json, and outside production the gap is recorded in
|
|
46
|
+
* `fr.missing.json` next to it.
|
|
47
|
+
*/
|
|
48
|
+
i18n: {
|
|
49
|
+
defaultLocale: "en",
|
|
50
|
+
supportedLocales: ["en", "fr"],
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* List-endpoint defaults, so every module paginates identically and no
|
|
55
|
+
* caller can ask for ten thousand rows. → modules/users/routes
|
|
56
|
+
*/
|
|
57
|
+
pagination: {
|
|
58
|
+
defaultLimit: 20,
|
|
59
|
+
maxLimit: 100,
|
|
60
|
+
},
|
|
61
|
+
})) satisfies AppConfigFn;
|
|
62
|
+
|
|
63
|
+
export default appConfig;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Types these keys at every call site: `config.pagination!.maxLimit` is a
|
|
67
|
+
* number, not `unknown`. `pnpm sync` does this automatically for
|
|
68
|
+
* module.config.ts factories; the app-level seed types itself here.
|
|
69
|
+
* Partial because the config object is empty until this factory runs.
|
|
70
|
+
*/
|
|
71
|
+
declare module "@tulipes/core/config" {
|
|
72
|
+
interface GlobalConfig extends Partial<ReturnType<typeof appConfig>> {}
|
|
73
|
+
}
|