@appweaver/create-weaver-app 1.5.0 → 1.6.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.
@@ -41,10 +41,10 @@ program
41
41
  .option('--bun', 'Use Bun as application runtime.')
42
42
  .option('--skipInstall', 'Skip all dependencies installation.')
43
43
  .option('--noDocker', 'Skip copying Dockerfile and docker-compose.yml files.')
44
- .option('--noRedis', 'Skip IoRedis package installation.')
45
- .option('--noQueue', 'Skip BullQueue package installation.')
46
- .option('--noMailer', 'Skip Nodemailer package installation.')
47
- .option('--noCron', 'Skip Cron package installation.')
44
+ .option('--noRedis', 'Skip IoRedis package installation and use in-memory cache, rate limit and queue.')
45
+ .option('--noQueue', 'Skip BullQueue package installation and use in-memory queue.')
46
+ .option('--noMailer', 'Skip Nodemailer package installation and disable the mailer.')
47
+ .option('--noCron', 'Skip Cron package installation and disable the scheduler.')
48
48
  .action(async (name, description, _, command) => {
49
49
  const directory = command.getOptionValue('outputDir');
50
50
  const runtime = command.getOptionValue('bun') ? 'bun' : 'node';
@@ -91,6 +91,7 @@ program
91
91
  }
92
92
  // Build database-specific docker-compose service blocks
93
93
  const dockerDb = getDatabaseDockerConfig(command, sanitizedName);
94
+ const dockerRedis = getRedisDockerConfig(command, sanitizedName);
94
95
  // Define all variables used in template files with .tpl extension
95
96
  const variables = {
96
97
  NAME: name.charAt(0).toUpperCase() + name.slice(1),
@@ -105,6 +106,9 @@ program
105
106
  DATABASE_DOCKER_MIGRATE_DEPENDS: dockerDb.migrateDepends,
106
107
  DATABASE_DOCKER_APP_VOLUME: dockerDb.appVolume,
107
108
  DATABASE_DOCKER_NAMED_VOLUME: dockerDb.namedVolume,
109
+ REDIS_DOCKER_SERVICE: dockerRedis.service,
110
+ REDIS_DOCKER_APP_DEPENDS: dockerRedis.appDepends,
111
+ REDIS_DOCKER_NAMED_VOLUME: dockerRedis.namedVolume,
108
112
  VERSION: pkg.version
109
113
  };
110
114
  // Process .tpl files: replace variables and remove .tpl extension
@@ -137,6 +141,15 @@ program
137
141
  }
138
142
  await promises_1.default.unlink(runtimeFile);
139
143
  }
144
+ // Configure the modules whose packages are skipped
145
+ const modulesConfig = getModulesConfig(command);
146
+ if (Object.keys(modulesConfig).length > 0) {
147
+ const configFile = node_path_1.default.join(destDir, 'appweaver.json');
148
+ const appConfig = JSON.parse(await promises_1.default.readFile(configFile, 'utf8'));
149
+ Object.assign(appConfig.config, modulesConfig);
150
+ await promises_1.default.writeFile(configFile, `${JSON.stringify(appConfig, null, 2)}
151
+ `, 'utf8');
152
+ }
140
153
  // Create test reports directory
141
154
  await promises_1.default.mkdir(node_path_1.default.join(destDir, 'reports'));
142
155
  // Create the SQLite database data directory (matches the "data/" DATABASE_URL)
@@ -250,6 +263,60 @@ function getDatabaseUrl(command, name, mode) {
250
263
  }
251
264
  return databaseUrl;
252
265
  }
266
+ /**
267
+ * Returns the configuration of the modules whose packages are skipped. The ones
268
+ * with an in-memory implementation switch to it, the others are disabled.
269
+ */
270
+ function getModulesConfig(command) {
271
+ const modulesConfig = {};
272
+ if (command.getOptionValue('noRedis')) {
273
+ modulesConfig.redis = { provider: '@appweaver/core/memory/in-memory' };
274
+ modulesConfig.cache = { provider: '@appweaver/core/cache/memory-cache' };
275
+ modulesConfig.rateLimit = { store: 'in-memory' };
276
+ }
277
+ // BullMQ also needs Redis
278
+ if (command.getOptionValue('noRedis') || command.getOptionValue('noQueue')) {
279
+ modulesConfig.queue = { provider: '@appweaver/core/queue/memory-queue' };
280
+ }
281
+ if (command.getOptionValue('noCron')) {
282
+ modulesConfig.scheduler = { enabled: false };
283
+ }
284
+ if (command.getOptionValue('noMailer')) {
285
+ modulesConfig.mailer = { enabled: false };
286
+ }
287
+ return modulesConfig;
288
+ }
289
+ function getRedisDockerConfig(command, name) {
290
+ if (command.getOptionValue('noRedis')) {
291
+ return { service: '', appDepends: '', namedVolume: '' };
292
+ }
293
+ return {
294
+ service: ` redis:
295
+ image: redis:7.4.9
296
+ container_name: ${name}-redis
297
+ restart: unless-stopped
298
+ healthcheck:
299
+ test: [ "CMD", "redis-cli", "ping" ]
300
+ interval: 30s
301
+ timeout: 10s
302
+ retries: 10
303
+ start_period: 5s
304
+ start_interval: 5s
305
+ ports:
306
+ - "127.0.0.1:6378:6379"
307
+ volumes:
308
+ - redis-data:/data
309
+ networks:
310
+ - ${name}
311
+
312
+ `,
313
+ appDepends: ` redis:
314
+ condition: service_healthy
315
+ `,
316
+ namedVolume: ` redis-data:
317
+ `
318
+ };
319
+ }
253
320
  function getDatabaseDockerConfig(command, name) {
254
321
  const database = command.getOptionValue('database').toLowerCase();
255
322
  // SQLite runs embedded in the application, so there is no database service.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appweaver/create-weaver-app",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Appweaver - the backend framework for AI-first development (@create-weaver-app)",
5
5
  "author": "Luka Matosevic",
6
6
  "license": "MIT",
package/skill/SKILL.md CHANGED
@@ -66,10 +66,10 @@ create-weaver-app <name> [description] [options]
66
66
  | `--bun` | Use Bun as application runtime. (default is node and npm) | false |
67
67
  | `--skipInstall` | Skip all dependencies installation. | false |
68
68
  | `--noDocker` | Skip Dockerfile, Dockerfile.bun and docker-compose.yml files | false |
69
- | `--noRedis` | Skip ioredis | false |
70
- | `--noQueue` | Skip bullmq | false |
71
- | `--noMailer` | Skip nodemailer | false |
72
- | `--noCron` | Skip cron | false |
69
+ | `--noRedis` | Skip ioredis, use in-memory cache, rate limit and queue | false |
70
+ | `--noQueue` | Skip bullmq, use in-memory queue | false |
71
+ | `--noMailer` | Skip nodemailer, disable mailer (email features respond 501) | false |
72
+ | `--noCron` | Skip cron, disable scheduler | false |
73
73
 
74
74
  **Example — PostgreSQL project without queue:**
75
75
 
@@ -15,6 +15,18 @@ import { Cache } from '@appweaver/common';
15
15
  const cache = inject(Cache);
16
16
  ```
17
17
 
18
+ ### Unavailable backend
19
+
20
+ With `CACHE_SKIP_ON_ERROR` enabled (the default) the cache never fails its callers. While the backing memory (e.g.
21
+ Redis) is down or a command fails, every method returns its empty result (`get` → `null`, `has`/`set`/`evict` →
22
+ `false`, `expire` → `0`, `keys` → `[]`), so the callers fall back to the database. The outage is logged once with an
23
+ error, and the recovery once with info. Entries written before an outage may have missed invalidations made during it,
24
+ so the whole cache is cleared once the memory is available again. Calling code does not need its own `try`/`catch`
25
+ around cache access.
26
+
27
+ Set `CACHE_SKIP_ON_ERROR` to `false` to make every method throw the memory error instead, so requests that read or
28
+ write the cache fail while its backend is down. Resource cache invalidation still only logs its errors.
29
+
18
30
  #### `cache.get<T>(key)`
19
31
 
20
32
  Retrieves a cached value. Returns `null` if the key does not exist.
@@ -167,6 +179,7 @@ const key = cacheService.buildCacheKey({
167
179
  | `CACHE_EVICTION_STRATEGY` | `enum` | `'lru'` | `lru`, `lfu`, or `fifo` |
168
180
  | `CACHE_INVALIDATION_STRATEGY` | `enum` | `'expire-related'` | `expire-related`, `expire-all`, or `none` |
169
181
  | `CACHE_INVALIDATION_DEFERRED` | `bool` | `false` | Fire invalidation in the background (non-blocking) |
182
+ | `CACHE_SKIP_ON_ERROR` | `bool` | `true` | Return empty results instead of failing on errors |
170
183
 
171
184
  Switch to the in-memory implementation for local development or tests:
172
185
 
@@ -46,10 +46,13 @@ weaver openapi|oa [options]
46
46
 
47
47
  Generate application OpenAPI specification schema.
48
48
 
49
- | Option | Description | Default |
50
- |---------------------------|------------------------------------------------------------------|-----------------|
51
- | `-o, --outputPath [path]` | Output path for generated OpenAPI specification | `./schema.json` |
52
- | `-f, --format [format]` | Output format for generated OpenAPI specification (json or yaml) | `json` |
49
+ | Option | Description | Default |
50
+ |---------------------------|------------------------------------------------------------------|------------------|
51
+ | `-o, --outputPath [path]` | Output path for generated OpenAPI specification | `./openapi.json` |
52
+ | `-f, --format [format]` | Output format for generated OpenAPI specification (json or yaml) | `json` |
53
+
54
+ When `--format yaml` is used and `--outputPath` is left at its default, the output path becomes `./openapi.yaml`.
55
+ Missing output directories are created automatically.
53
56
 
54
57
  ---
55
58
 
@@ -80,9 +83,12 @@ Per model, the type file holds `<Model>`, `<Model>Single`, `<Model>Multiple`, `<
80
83
  ## `weaver migrate`
81
84
 
82
85
  ```
83
- weaver migrate|mge [options]
86
+ weaver migrate|mge
84
87
  ```
85
88
 
89
+ Apply all pending database migrations (`prisma migrate deploy`). Takes no options, creates nothing, and never
90
+ prompts, which makes it the command to run in CI, in containers, and in production.
91
+
86
92
  ---
87
93
 
88
94
  ## `weaver migration`
@@ -102,10 +108,15 @@ Database migration commands.
102
108
 
103
109
  ### `weaver migration reset`
104
110
 
105
- | Option | Description | Default |
106
- |---------------|----------------------------------------------|---------|
107
- | `-f, --force` | Force reset for non-development environments | `false` |
108
- | `-y, --yes` | Skip confirmation prompt | `false` |
111
+ Drops the database, recreates it, and re-applies every migration (`prisma migrate reset`). **All data is lost.**
112
+
113
+ | Option | Description | Default |
114
+ |---------------|-----------------------------------------------------------|---------|
115
+ | `-f, --force` | Allow the reset outside the `dev` and `test` environments | `false` |
116
+ | `-y, --yes` | Skip confirmation prompt | `false` |
117
+
118
+ Without `--force` the command runs only when `NODE_ENV` resolves to `dev` or `test`, and aborts otherwise. Either
119
+ `--force` or `--yes` skips the Prisma confirmation prompt.
109
120
 
110
121
  ---
111
122
 
@@ -117,13 +128,13 @@ weaver seed|sd [options]
117
128
 
118
129
  Seed the database.
119
130
 
120
- | Option | Description | Default |
121
- |-------------------------|---------------------------------------------------------------------|-----------------------|
122
- | `--seedersPath [path]` | Seeders directory path | `config.SEEDERS_PATH` |
123
- | `-b, --buildProject` | Build the project before seeding | `false` |
124
- | `-p, --project` | TypeScript project build config file (used when `-b` is set) | `tsconfig.build.json` |
125
- | `-c, --continueOnError` | Continue seeder execution if error is thrown | `false` |
126
- | `-f, --fixWarnings` | Fix all seeder warnings like wrong checksum or deleted seeder files | `false` |
131
+ | Option | Description | Default |
132
+ |-------------------------|---------------------------------------------------------------------|------------------------------------|
133
+ | `--seedersPath [path]` | Seeders directory path | `config.DATABASE_SEEDERS_DIR_PATH` |
134
+ | `-b, --buildProject` | Build the project before seeding | `false` |
135
+ | `-p, --project` | TypeScript project build config file (used when `-b` is set) | `tsconfig.build.json` |
136
+ | `-c, --continueOnError` | Continue seeder execution if error is thrown | `false` |
137
+ | `-f, --fixWarnings` | Fix all seeder warnings like wrong checksum or deleted seeder files | `false` |
127
138
 
128
139
  ---
129
140
 
@@ -135,10 +146,10 @@ weaver start|s [options]
135
146
 
136
147
  Start the application.
137
148
 
138
- | Option | Description | Default |
139
- |-----------------|-------------------------------------------------------------|-----------------|
140
- | `-p, --project` | TypeScript project config file | `tsconfig.json` |
141
- | `-w, --watch` | Run in watch mode (recompiles and restarts on file changes) | `false` |
149
+ | Option | Description | Default |
150
+ |-----------------|-------------------------------------------------------------|-----------------------|
151
+ | `-p, --project` | TypeScript project config file | `tsconfig.build.json` |
152
+ | `-w, --watch` | Run in watch mode (recompiles and restarts on file changes) | `false` |
142
153
 
143
154
  ---
144
155
 
@@ -171,6 +182,9 @@ Requires `NODE_ENV=test`.
171
182
  | `--migrationName [name]` | Name for the initial migration | `init_test` |
172
183
  | `--verbose` | Print verbose output | `false` |
173
184
 
185
+ Aborts unless the storage, schema and client paths all resolve inside `--dir`, so a misconfigured test run cannot
186
+ touch the development database or uploads.
187
+
174
188
  ### `weaver test reset`
175
189
 
176
190
  Requires `NODE_ENV=test`. With no flags, resets both database and storage.
@@ -120,13 +120,14 @@ The config object is frozen with `Object.freeze()` after loading to prevent runt
120
120
 
121
121
  ### Rate limiting (RATE_LIMIT_\*)
122
122
 
123
- | Property | Type | Default | Description |
124
- |-------------------------|-----------|-----------|------------------------------------------------------------------|
125
- | `RATE_LIMIT_ENABLED` | boolean | `true` | Enable global rate limiting middleware. |
126
- | `RATE_LIMIT_MAX` | integer | `1000` | Maximum requests allowed per time window. |
127
- | `RATE_LIMIT_WINDOW` | integer | `60000` | Rate limit window in milliseconds. |
128
- | `RATE_LIMIT_ALLOW_LIST` | string[]? | - | IP addresses/patterns exempt from rate limiting. |
129
- | `RATE_LIMIT_STORE` | enum | `'redis'` | Store backend for tracking limits. Values: `redis`, `in-memory`. |
123
+ | Property | Type | Default | Description |
124
+ |----------------------------|-----------|-----------|------------------------------------------------------------------------------------------------------------|
125
+ | `RATE_LIMIT_ENABLED` | boolean | `true` | Enable global rate limiting middleware. |
126
+ | `RATE_LIMIT_MAX` | integer | `1000` | Maximum requests allowed per time window. |
127
+ | `RATE_LIMIT_WINDOW` | integer | `60000` | Rate limit window in milliseconds. |
128
+ | `RATE_LIMIT_ALLOW_LIST` | string[]? | - | IP addresses/patterns exempt from rate limiting. |
129
+ | `RATE_LIMIT_STORE` | enum | `'redis'` | Store backend for tracking limits. Values: `redis`, `in-memory`. |
130
+ | `RATE_LIMIT_SKIP_ON_ERROR` | boolean | `true` | Skip rate limiting instead of failing requests with `500` when the store errors, e.g. while Redis is down. |
130
131
 
131
132
  ### Swagger / OpenAPI (SWAGGER\_\*)
132
133
 
@@ -182,14 +183,14 @@ The config object is frozen with `Object.freeze()` after loading to prevent runt
182
183
 
183
184
  #### General
184
185
 
185
- | Property | Type | Default | Description |
186
- |--------------------------------------|----------|---------------------------------------------------------|--------------------------------------------------|
187
- | `SECURITY_ROUTE_PREFIX` | string | `'/auth'` | Base path for authentication routes. |
188
- | `SECURITY_CACHE_TTL` | integer | `300000` | Security cache TTL in milliseconds. |
189
- | `SECURITY_AUTH_OTT_TTL` | integer | `120000` | One-time token TTL for authentication (ms). |
190
- | `SECURITY_ALLOWED_REDIRECT_HOSTS` | string[] | `['*']` | Allowed hosts for post-authentication redirects. |
191
- | `SECURITY_STORE_PROVIDER` | string | `'@appweaver/core/security/store/redis-security-store'` | Security store implementation path. |
192
- | `SECURITY_STORE_KEEP_DATABASE_TABLE` | boolean | `false` | Keep database table after migrations. |
186
+ | Property | Type | Default | Description |
187
+ |--------------------------------------|----------|------------------------------------------------------------|-----------------------------------------------------------|
188
+ | `SECURITY_ROUTE_PREFIX` | string | `'/auth'` | Base path for authentication routes. |
189
+ | `SECURITY_CACHE_TTL` | integer | `300000` | Security cache TTL in milliseconds. |
190
+ | `SECURITY_AUTH_OTT_TTL` | integer | `120000` | One-time token TTL for authentication (ms). |
191
+ | `SECURITY_ALLOWED_REDIRECT_HOSTS` | string[] | `['*']` | Allowed hosts for post-authentication redirects. |
192
+ | `SECURITY_STORE_PROVIDER` | string | `'@appweaver/core/security/store/database-security-store'` | Security store implementation path. |
193
+ | `SECURITY_STORE_KEEP_DATABASE_TABLE` | boolean | `false` | Keep the one-time token table when another store is used. |
193
194
 
194
195
  #### Password policy
195
196
 
@@ -390,6 +391,12 @@ you generated yourself, or provide the team ID, key ID and `.p8` private key and
390
391
  | `REDIS_URL` | string | `'redis://localhost:6379/0'` | Redis connection URL. |
391
392
  | `REDIS_PROVIDER` | string | `'@appweaver/core/memory/redis'` | Redis provider implementation path. |
392
393
 
394
+ The application starts and keeps running while Redis is unreachable. Connections reconnect in the background, each
395
+ outage and recovery is logged once, and commands fail right away instead of waiting for the reconnection, so by default
396
+ the
397
+ cache falls back to the database (see `CACHE_SKIP_ON_ERROR`), rate limiting is skipped (see
398
+ `RATE_LIMIT_SKIP_ON_ERROR`) and queues throw (see `queue.md`).
399
+
393
400
  ### In-memory store (MEMORY\_\*)
394
401
 
395
402
  | Property | Type | Default | Description |
@@ -412,6 +419,7 @@ you generated yourself, or provide the team ID, key ID and `.p8` private key and
412
419
  | `CACHE_EVICTION_DEFERRED` | boolean | `false` | Defer eviction to a background process. |
413
420
  | `CACHE_INVALIDATION_STRATEGY` | enum | `'expire-related'` | Invalidation strategy. Values: `expire-related`, `expire-all`, `none`. |
414
421
  | `CACHE_INVALIDATION_DEFERRED` | boolean | `false` | Defer invalidation to a background process. |
422
+ | `CACHE_SKIP_ON_ERROR` | boolean | `true` | Return empty results instead of failing when the cache backend errors. |
415
423
  | `CACHE_PROVIDER` | string | `'@appweaver/core/cache/redis-cache'` | Cache provider implementation path. |
416
424
 
417
425
  ### Job queue (QUEUE\_\*)
@@ -429,10 +437,11 @@ you generated yourself, or provide the team ID, key ID and `.p8` private key and
429
437
 
430
438
  ### Scheduler (SCHEDULER\_\*)
431
439
 
432
- | Property | Type | Default | Description |
433
- |----------------------------|---------|----------------------------------------------|---------------------------------------------------|
434
- | `SCHEDULER_AUTO_START_JOB` | boolean | `true` | Auto-start scheduled jobs on application startup. |
435
- | `SCHEDULER_PROVIDER` | string | `'@appweaver/core/scheduler/cron-scheduler'` | Scheduler provider implementation path. |
440
+ | Property | Type | Default | Description |
441
+ |----------------------------|---------|----------------------------------------------|----------------------------------------------------------------------------|
442
+ | `SCHEDULER_ENABLED` | boolean | `true` | Enable the scheduler. Disable it when the `cron` package is not installed. |
443
+ | `SCHEDULER_AUTO_START_JOB` | boolean | `true` | Auto-start scheduled jobs on application startup. |
444
+ | `SCHEDULER_PROVIDER` | string | `'@appweaver/core/scheduler/cron-scheduler'` | Scheduler provider implementation path. |
436
445
 
437
446
  ### Events (EVENTS\_\*)
438
447
 
@@ -443,16 +452,17 @@ you generated yourself, or provide the team ID, key ID and `.p8` private key and
443
452
 
444
453
  ### Mailer (MAILER\_\*)
445
454
 
446
- | Property | Type | Default | Description |
447
- |-------------------------|---------|----------------------------------------|------------------------------------------|
448
- | `MAILER_SENDER_NAME` | string? | - | Default sender name for outgoing emails. |
449
- | `MAILER_SENDER_ADDRESS` | string? | - | Default sender email address. |
450
- | `MAILER_PROVIDER` | string | `'@appweaver/core/mailer/smtp-mailer'` | Mailer provider implementation path. |
451
- | `MAILER_SMTP_HOST` | string | `'127.0.0.1'` | SMTP server hostname. |
452
- | `MAILER_SMTP_PORT` | integer | `587` | SMTP server port. |
453
- | `MAILER_SMTP_SECURE` | boolean | `false` | Use TLS/SSL for SMTP connections. |
454
- | `MAILER_SMTP_USER` | string? | - | SMTP authentication username. |
455
- | `MAILER_SMTP_PASSWORD` | string? | - | SMTP authentication password. |
455
+ | Property | Type | Default | Description |
456
+ |-------------------------|---------|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
457
+ | `MAILER_ENABLED` | boolean | `true` | Enable the mailer. Disable it when the `nodemailer` package is not installed. Without it the email features (email verification, password reset, 2FA) respond with `501`. |
458
+ | `MAILER_SENDER_NAME` | string? | - | Default sender name for outgoing emails. |
459
+ | `MAILER_SENDER_ADDRESS` | string? | - | Default sender email address. |
460
+ | `MAILER_PROVIDER` | string | `'@appweaver/core/mailer/smtp-mailer'` | Mailer provider implementation path. |
461
+ | `MAILER_SMTP_HOST` | string | `'127.0.0.1'` | SMTP server hostname. |
462
+ | `MAILER_SMTP_PORT` | integer | `587` | SMTP server port. |
463
+ | `MAILER_SMTP_SECURE` | boolean | `false` | Use TLS/SSL for SMTP connections. |
464
+ | `MAILER_SMTP_USER` | string? | - | SMTP authentication username. |
465
+ | `MAILER_SMTP_PASSWORD` | string? | - | SMTP authentication password. |
456
466
 
457
467
  ### System (SYSTEM\_\*)
458
468
 
@@ -45,6 +45,12 @@ await queue.closeAll();
45
45
 
46
46
  Returns a `HealthCheckResult` indicating whether the underlying queue backend is reachable.
47
47
 
48
+ ### Unavailable backend
49
+
50
+ `BullQueue` needs Redis and has no fallback. The application still starts while Redis is down, but `sendJob` and
51
+ `sendBulkJobs` throw `Queue '<name>' is unavailable, Redis connection is not ready` right away instead of waiting for
52
+ the reconnection, so callers that must not fail should catch it. Workers pick jobs up again once Redis reconnects.
53
+
48
54
  ---
49
55
 
50
56
  ## `QueueProcessor` — per-queue API
@@ -76,7 +76,8 @@ if `SECURITY_JWT_SECRET` is set.
76
76
  "source": "password | apiKey | basic | oauth2Google | oauth2Facebook | oauth2X | oauth2Github | oauth2Gitlab | oauth2Linkedin | oauth2Apple | oauth2Microsoft | oauth2Custom",
77
77
  "username": "User email (e.g. admin@example.com)",
78
78
  "sub": "User ID (e.g. 123)",
79
- "iat": "Issued at timestamp (e.g. 1774623924234)"
79
+ "iat": "Issued at, in seconds (e.g. 1774623924)",
80
+ "exp": "Expiration, in seconds (e.g. 1777215924)"
80
81
  }
81
82
  ```
82
83
 
@@ -85,9 +86,11 @@ if `SECURITY_JWT_SECRET` is set.
85
86
  On every authenticated request, the server:
86
87
 
87
88
  1. Verifies the JWT signature
88
- 2. Loads the user from the database by `sub` (user ID)
89
+ 2. Loads the user by `sub` (user ID), from the cache when possible. Every update or delete of the auth user, through
90
+ the auth service (logout, password change or reset) or the resource routes, evicts the cached user regardless of
91
+ the cache invalidation strategy
89
92
  3. Checks that the user is enabled
90
- 4. Validates `logoutAt` is before the token's `iat` (tokens issued before logout are rejected)
93
+ 4. Rejects tokens issued before the user's `logoutAt` (compared in whole seconds)
91
94
  5. Checks that the token scope allows access to the requested URL
92
95
 
93
96
  ### Auth routes
@@ -428,7 +431,7 @@ update: {
428
431
  On every authenticated request:
429
432
 
430
433
  1. Verify user exists and is enabled
431
- 2. Verify `logoutAt` is before token `iat`
434
+ 2. Verify the token was not issued before `logoutAt`
432
435
  3. Verify JWT scope allows access to the URL
433
436
  4. Verify the user has required roles (if configured)
434
437
  5. Verify the user has required permissions (if configured)
@@ -629,10 +632,24 @@ time-limited.
629
632
 
630
633
  ### Storage
631
634
 
632
- OTTs are stored in the configured security store:
635
+ OTTs are stored in the security store set by `SECURITY_STORE_PROVIDER`. Only a hash of each token is stored.
633
636
 
634
- - **Redis** (default): `@appweaver/core/security/store/redis-security-store`
635
- - **Database**: `@appweaver/core/security/store/database-security-store`
637
+ - **Database** (default): `@appweaver/core/security/store/database-security-store`. Stores the tokens in the
638
+ `OneTimeToken` model, which is added to the schema only while this store is configured (or
639
+ `SECURITY_STORE_KEEP_DATABASE_TABLE` is set). The model has no routes.
640
+ - **Redis**: `@appweaver/core/security/store/redis-security-store`. Stores the tokens as Redis keys with a TTL, so the
641
+ one-time token flows (OAuth2 login, 2FA, email verification, password reset) fail while Redis is unavailable.
642
+
643
+ Switching the store adds or removes the `OneTimeToken` model, so run `weaver generate` and create a migration
644
+ afterwards.
645
+
646
+ Both stores behave the same way:
647
+
648
+ - A token is consumed by its first successful use. Of concurrent uses of the same token only one succeeds, the others
649
+ are rejected with `401`.
650
+ - A token failing the content validation (e.g. a mistyped 2FA code) is kept, so it can be used again until it expires.
651
+ - Expired tokens are rejected. The database store deletes them whenever a new token is created, Redis expires them on
652
+ its own.
636
653
 
637
654
  ---
638
655
 
@@ -1,23 +1,5 @@
1
1
  services:
2
- {{DATABASE_DOCKER_SERVICE}} redis:
3
- image: redis:7.4.9
4
- container_name: {{LOWER_NAME}}-redis
5
- restart: unless-stopped
6
- healthcheck:
7
- test: [ "CMD", "redis-cli", "ping" ]
8
- interval: 30s
9
- timeout: 10s
10
- retries: 10
11
- start_period: 5s
12
- start_interval: 5s
13
- ports:
14
- - "127.0.0.1:6378:6379"
15
- volumes:
16
- - redis-data:/data
17
- networks:
18
- - {{LOWER_NAME}}
19
-
20
- {{LOWER_NAME}}.migrations:
2
+ {{DATABASE_DOCKER_SERVICE}}{{REDIS_DOCKER_SERVICE}} {{LOWER_NAME}}.migrations:
21
3
  image: {{LOWER_NAME}}:latest
22
4
  container_name: {{LOWER_NAME}}-migrations
23
5
  restart: no
@@ -64,9 +46,7 @@ services:
64
46
  start_period: 5s
65
47
  start_interval: 5s
66
48
  depends_on:
67
- redis:
68
- condition: service_healthy
69
- {{LOWER_NAME}}.seed:
49
+ {{REDIS_DOCKER_APP_DEPENDS}} {{LOWER_NAME}}.seed:
70
50
  condition: service_completed_successfully
71
51
  ports:
72
52
  - "127.0.0.1:{{PORT}}:{{PORT}}"
@@ -84,4 +64,4 @@ networks:
84
64
  name: {{LOWER_NAME}}-network
85
65
 
86
66
  volumes:
87
- {{DATABASE_DOCKER_NAMED_VOLUME}} redis-data:
67
+ {{DATABASE_DOCKER_NAMED_VOLUME}}{{REDIS_DOCKER_NAMED_VOLUME}}