create-authenik8-app 2.4.14 → 2.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +34 -516
  2. package/dist/src/bin/cli.d.ts +3 -0
  3. package/dist/src/bin/cli.d.ts.map +1 -0
  4. package/dist/src/bin/cli.js +112 -0
  5. package/dist/src/bin/cli.js.map +1 -0
  6. package/dist/src/bin/index.js +108 -23
  7. package/dist/src/bin/index.js.map +1 -1
  8. package/dist/src/commands/add/apply.d.ts +6 -0
  9. package/dist/src/commands/add/apply.d.ts.map +1 -0
  10. package/dist/src/commands/add/apply.js +123 -0
  11. package/dist/src/commands/add/apply.js.map +1 -0
  12. package/dist/src/commands/add/ci.d.ts +6 -0
  13. package/dist/src/commands/add/ci.d.ts.map +1 -0
  14. package/dist/src/commands/add/ci.js +108 -0
  15. package/dist/src/commands/add/ci.js.map +1 -0
  16. package/dist/src/commands/add/context.d.ts +5 -0
  17. package/dist/src/commands/add/context.d.ts.map +1 -0
  18. package/dist/src/commands/add/context.js +19 -0
  19. package/dist/src/commands/add/context.js.map +1 -0
  20. package/dist/src/commands/add/diff.d.ts +4 -0
  21. package/dist/src/commands/add/diff.d.ts.map +1 -0
  22. package/dist/src/commands/add/diff.js +114 -0
  23. package/dist/src/commands/add/diff.js.map +1 -0
  24. package/dist/src/commands/add/index.d.ts +6 -0
  25. package/dist/src/commands/add/index.d.ts.map +1 -0
  26. package/dist/src/commands/add/index.js +61 -0
  27. package/dist/src/commands/add/index.js.map +1 -0
  28. package/dist/src/commands/add/output.d.ts +5 -0
  29. package/dist/src/commands/add/output.d.ts.map +1 -0
  30. package/dist/src/commands/add/output.js +58 -0
  31. package/dist/src/commands/add/output.js.map +1 -0
  32. package/dist/src/commands/add/plan.d.ts +6 -0
  33. package/dist/src/commands/add/plan.d.ts.map +1 -0
  34. package/dist/src/commands/add/plan.js +93 -0
  35. package/dist/src/commands/add/plan.js.map +1 -0
  36. package/dist/src/commands/add/registry.d.ts +4 -0
  37. package/dist/src/commands/add/registry.d.ts.map +1 -0
  38. package/dist/src/commands/add/registry.js +37 -0
  39. package/dist/src/commands/add/registry.js.map +1 -0
  40. package/dist/src/commands/add/types.d.ts +37 -0
  41. package/dist/src/commands/add/types.d.ts.map +1 -0
  42. package/dist/src/commands/add/types.js +2 -0
  43. package/dist/src/commands/add/types.js.map +1 -0
  44. package/dist/src/commands/doctor/checks.d.ts +3 -0
  45. package/dist/src/commands/doctor/checks.d.ts.map +1 -0
  46. package/dist/src/commands/doctor/checks.js +353 -0
  47. package/dist/src/commands/doctor/checks.js.map +1 -0
  48. package/dist/src/commands/doctor/context.d.ts +5 -0
  49. package/dist/src/commands/doctor/context.d.ts.map +1 -0
  50. package/dist/src/commands/doctor/context.js +134 -0
  51. package/dist/src/commands/doctor/context.js.map +1 -0
  52. package/dist/src/commands/doctor/index.d.ts +9 -0
  53. package/dist/src/commands/doctor/index.d.ts.map +1 -0
  54. package/dist/src/commands/doctor/index.js +80 -0
  55. package/dist/src/commands/doctor/index.js.map +1 -0
  56. package/dist/src/commands/doctor/output.d.ts +4 -0
  57. package/dist/src/commands/doctor/output.d.ts.map +1 -0
  58. package/dist/src/commands/doctor/output.js +53 -0
  59. package/dist/src/commands/doctor/output.js.map +1 -0
  60. package/dist/src/commands/doctor/postGeneration.d.ts +7 -0
  61. package/dist/src/commands/doctor/postGeneration.d.ts.map +1 -0
  62. package/dist/src/commands/doctor/postGeneration.js +22 -0
  63. package/dist/src/commands/doctor/postGeneration.js.map +1 -0
  64. package/dist/src/commands/doctor/services.d.ts +5 -0
  65. package/dist/src/commands/doctor/services.d.ts.map +1 -0
  66. package/dist/src/commands/doctor/services.js +80 -0
  67. package/dist/src/commands/doctor/services.js.map +1 -0
  68. package/dist/src/commands/doctor/types.d.ts +69 -0
  69. package/dist/src/commands/doctor/types.d.ts.map +1 -0
  70. package/dist/src/commands/doctor/types.js +2 -0
  71. package/dist/src/commands/doctor/types.js.map +1 -0
  72. package/dist/src/commands/upgrade/context.d.ts +5 -0
  73. package/dist/src/commands/upgrade/context.d.ts.map +1 -0
  74. package/dist/src/commands/upgrade/context.js +57 -0
  75. package/dist/src/commands/upgrade/context.js.map +1 -0
  76. package/dist/src/commands/upgrade/index.d.ts +7 -0
  77. package/dist/src/commands/upgrade/index.d.ts.map +1 -0
  78. package/dist/src/commands/upgrade/index.js +35 -0
  79. package/dist/src/commands/upgrade/index.js.map +1 -0
  80. package/dist/src/commands/upgrade/migrations.d.ts +4 -0
  81. package/dist/src/commands/upgrade/migrations.d.ts.map +1 -0
  82. package/dist/src/commands/upgrade/migrations.js +22 -0
  83. package/dist/src/commands/upgrade/migrations.js.map +1 -0
  84. package/dist/src/commands/upgrade/output.d.ts +4 -0
  85. package/dist/src/commands/upgrade/output.d.ts.map +1 -0
  86. package/dist/src/commands/upgrade/output.js +52 -0
  87. package/dist/src/commands/upgrade/output.js.map +1 -0
  88. package/dist/src/commands/upgrade/plan.d.ts +3 -0
  89. package/dist/src/commands/upgrade/plan.d.ts.map +1 -0
  90. package/dist/src/commands/upgrade/plan.js +119 -0
  91. package/dist/src/commands/upgrade/plan.js.map +1 -0
  92. package/dist/src/commands/upgrade/types.d.ts +48 -0
  93. package/dist/src/commands/upgrade/types.d.ts.map +1 -0
  94. package/dist/src/commands/upgrade/types.js +2 -0
  95. package/dist/src/commands/upgrade/types.js.map +1 -0
  96. package/dist/src/lib/args.d.ts +21 -0
  97. package/dist/src/lib/args.d.ts.map +1 -1
  98. package/dist/src/lib/args.js +119 -1
  99. package/dist/src/lib/args.js.map +1 -1
  100. package/dist/src/lib/constants.d.ts +1 -1
  101. package/dist/src/lib/constants.d.ts.map +1 -1
  102. package/dist/src/lib/constants.js +3 -1
  103. package/dist/src/lib/constants.js.map +1 -1
  104. package/dist/src/lib/expressOAuth.d.ts +10 -0
  105. package/dist/src/lib/expressOAuth.d.ts.map +1 -0
  106. package/dist/src/lib/expressOAuth.js +164 -0
  107. package/dist/src/lib/expressOAuth.js.map +1 -0
  108. package/dist/src/lib/interruption.d.ts +14 -0
  109. package/dist/src/lib/interruption.d.ts.map +1 -0
  110. package/dist/src/lib/interruption.js +16 -0
  111. package/dist/src/lib/interruption.js.map +1 -0
  112. package/dist/src/lib/nonInteractive.d.ts +8 -0
  113. package/dist/src/lib/nonInteractive.d.ts.map +1 -0
  114. package/dist/src/lib/nonInteractive.js +111 -0
  115. package/dist/src/lib/nonInteractive.js.map +1 -0
  116. package/dist/src/lib/oauth.d.ts +9 -0
  117. package/dist/src/lib/oauth.d.ts.map +1 -0
  118. package/dist/src/lib/oauth.js +62 -0
  119. package/dist/src/lib/oauth.js.map +1 -0
  120. package/dist/src/lib/onboarding.d.ts +8 -0
  121. package/dist/src/lib/onboarding.d.ts.map +1 -0
  122. package/dist/src/lib/onboarding.js +35 -0
  123. package/dist/src/lib/onboarding.js.map +1 -0
  124. package/dist/src/lib/packageManagerCommands.d.ts +11 -0
  125. package/dist/src/lib/packageManagerCommands.d.ts.map +1 -0
  126. package/dist/src/lib/packageManagerCommands.js +34 -0
  127. package/dist/src/lib/packageManagerCommands.js.map +1 -0
  128. package/dist/src/lib/process.d.ts.map +1 -1
  129. package/dist/src/lib/process.js +9 -0
  130. package/dist/src/lib/process.js.map +1 -1
  131. package/dist/src/lib/projectManifest.d.ts +67 -0
  132. package/dist/src/lib/projectManifest.d.ts.map +1 -0
  133. package/dist/src/lib/projectManifest.js +134 -0
  134. package/dist/src/lib/projectManifest.js.map +1 -0
  135. package/dist/src/lib/release.d.ts +9 -0
  136. package/dist/src/lib/release.d.ts.map +1 -0
  137. package/dist/src/lib/release.js +48 -0
  138. package/dist/src/lib/release.js.map +1 -0
  139. package/dist/src/lib/rootCommand.d.ts +15 -0
  140. package/dist/src/lib/rootCommand.d.ts.map +1 -0
  141. package/dist/src/lib/rootCommand.js +17 -0
  142. package/dist/src/lib/rootCommand.js.map +1 -0
  143. package/dist/src/lib/schemas.d.ts +15 -6
  144. package/dist/src/lib/schemas.d.ts.map +1 -1
  145. package/dist/src/lib/schemas.js +8 -1
  146. package/dist/src/lib/schemas.js.map +1 -1
  147. package/dist/src/lib/semver.d.ts +10 -0
  148. package/dist/src/lib/semver.d.ts.map +1 -0
  149. package/dist/src/lib/semver.js +51 -0
  150. package/dist/src/lib/semver.js.map +1 -0
  151. package/dist/src/lib/types.d.ts +5 -1
  152. package/dist/src/lib/types.d.ts.map +1 -1
  153. package/dist/src/steps/configurePrisma.d.ts.map +1 -1
  154. package/dist/src/steps/configurePrisma.js +18 -23
  155. package/dist/src/steps/configurePrisma.js.map +1 -1
  156. package/dist/src/steps/configureReadme.d.ts +4 -0
  157. package/dist/src/steps/configureReadme.d.ts.map +1 -0
  158. package/dist/src/steps/configureReadme.js +22 -0
  159. package/dist/src/steps/configureReadme.js.map +1 -0
  160. package/dist/src/steps/createProject.d.ts.map +1 -1
  161. package/dist/src/steps/createProject.js +16 -170
  162. package/dist/src/steps/createProject.js.map +1 -1
  163. package/dist/src/steps/finalSetup.d.ts +3 -2
  164. package/dist/src/steps/finalSetup.d.ts.map +1 -1
  165. package/dist/src/steps/finalSetup.js +14 -9
  166. package/dist/src/steps/finalSetup.js.map +1 -1
  167. package/dist/src/steps/installDeps.d.ts +1 -0
  168. package/dist/src/steps/installDeps.d.ts.map +1 -1
  169. package/dist/src/steps/installDeps.js +11 -0
  170. package/dist/src/steps/installDeps.js.map +1 -1
  171. package/dist/src/steps/prompts.d.ts.map +1 -1
  172. package/dist/src/steps/prompts.js +32 -13
  173. package/dist/src/steps/prompts.js.map +1 -1
  174. package/dist/src/utils/output.d.ts.map +1 -1
  175. package/dist/src/utils/output.js +23 -11
  176. package/dist/src/utils/output.js.map +1 -1
  177. package/package.json +5 -4
  178. package/templates/express-auth/README.md +21 -12
  179. package/templates/express-auth/package.json +11 -7
  180. package/templates/express-auth/src/config/redis.ts +109 -0
  181. package/templates/express-auth/src/server.ts +2 -0
  182. package/templates/express-auth+/README.md +25 -18
  183. package/templates/express-auth+/package.json +11 -7
  184. package/templates/express-auth+/src/auth/auth.ts +2 -0
  185. package/templates/express-auth+/src/auth/controllers/password.controller.ts +12 -1
  186. package/templates/express-auth+/src/auth/routes/password.route.ts +1 -0
  187. package/templates/express-auth+/src/config/redis.ts +109 -0
  188. package/templates/express-auth+/src/utils/security.ts +12 -0
  189. package/templates/express-base/README.md +21 -12
  190. package/templates/express-base/package.json +11 -8
  191. package/templates/express-base/src/.env.example +1 -1
  192. package/templates/express-base/src/config/redis.ts +109 -0
  193. package/templates/express-base/src/server.ts +2 -0
  194. package/templates/fullstack/.env.example +3 -2
  195. package/templates/fullstack/PRESET_CONTRACT.md +7 -3
  196. package/templates/fullstack/README.md +45 -7
  197. package/templates/fullstack/apps/api/package.json +6 -4
  198. package/templates/fullstack/apps/api/prisma/migrations/20260721000000_init/migration.sql +163 -0
  199. package/templates/fullstack/apps/api/prisma/migrations/migration_lock.toml +3 -0
  200. package/templates/fullstack/apps/api/src/auth/authenik8.ts +13 -4
  201. package/templates/fullstack/apps/api/src/config/env.ts +9 -1
  202. package/templates/fullstack/apps/api/src/config/logger.ts +21 -11
  203. package/templates/fullstack/apps/api/tests/logger.test.ts +43 -0
  204. package/templates/fullstack/apps/web/package.json +3 -1
  205. package/templates/fullstack/apps/web/src/App.tsx +1 -1
  206. package/templates/fullstack/apps/web/src/auth/RouteGuards.tsx +1 -1
  207. package/templates/fullstack/apps/web/src/components/AppShell.tsx +90 -45
  208. package/templates/fullstack/apps/web/src/components/AuthShell.tsx +10 -7
  209. package/templates/fullstack/apps/web/src/components/Page.tsx +6 -3
  210. package/templates/fullstack/apps/web/src/features/projects/ProjectPages.tsx +1 -1
  211. package/templates/fullstack/apps/web/src/main.tsx +13 -6
  212. package/templates/fullstack/apps/web/src/pages/DashboardPage.tsx +163 -23
  213. package/templates/fullstack/apps/web/src/pages/StatusPages.tsx +1 -1
  214. package/templates/fullstack/apps/web/src/pages/auth/LoginPage.tsx +93 -17
  215. package/templates/fullstack/apps/web/src/pages/auth/OAuthCallbackPage.tsx +1 -1
  216. package/templates/fullstack/apps/web/src/pages/auth/RecoveryPages.tsx +1 -1
  217. package/templates/fullstack/apps/web/src/pages/auth/RegisterPage.tsx +1 -1
  218. package/templates/fullstack/apps/web/src/pages/auth/VerifyEmailPage.tsx +1 -1
  219. package/templates/fullstack/apps/web/src/styles.css +119 -43
  220. package/templates/fullstack/apps/web/vite.config.ts +12 -0
  221. package/templates/fullstack/docker-compose.yml +2 -2
  222. package/templates/fullstack/gitignore.template +1 -0
  223. package/templates/fullstack/package.json +17 -8
  224. package/templates/fullstack/scripts/run-local.mjs +157 -0
  225. package/templates/prisma/postgresql/.env.example +1 -2
  226. package/templates/prisma/sqlite/.env.example +1 -2
  227. package/templates/prisma/sqlite/client.ts +2 -2
@@ -0,0 +1,109 @@
1
+ import Redis, { type Redis as RedisClient } from "ioredis";
2
+
3
+ const localRedisUrl = "memory://";
4
+ const redisOptions = {
5
+ connectTimeout: 5_000,
6
+ enableReadyCheck: true,
7
+ maxRetriesPerRequest: 2,
8
+ };
9
+
10
+ function externalRedisUrl(source: string): string {
11
+ let url: URL;
12
+ try {
13
+ url = new URL(source);
14
+ } catch {
15
+ throw new Error("REDIS_URL must be memory://, redis://, or rediss://");
16
+ }
17
+
18
+ if (url.protocol !== "redis:" && url.protocol !== "rediss:") {
19
+ throw new Error("REDIS_URL must be memory://, redis://, or rediss://");
20
+ }
21
+
22
+ return source;
23
+ }
24
+
25
+ function redisPort(): number {
26
+ const port = Number(process.env.REDIS_PORT?.trim() || "6379");
27
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
28
+ throw new Error("REDIS_PORT must be an integer from 1 to 65535");
29
+ }
30
+ return port;
31
+ }
32
+
33
+ function waitForRedis(client: RedisClient): Promise<RedisClient> {
34
+ return new Promise((resolve, reject) => {
35
+ let settled = false;
36
+ let readinessCheckStarted = false;
37
+ const cleanup = () => {
38
+ client.off("ready", handleReady);
39
+ client.off("error", handleError);
40
+ };
41
+ const finish = (error?: Error) => {
42
+ if (settled) return;
43
+ settled = true;
44
+ cleanup();
45
+ if (error) {
46
+ client.disconnect();
47
+ reject(error);
48
+ } else {
49
+ resolve(client);
50
+ }
51
+ };
52
+ const handleReady = () => {
53
+ if (readinessCheckStarted) return;
54
+ readinessCheckStarted = true;
55
+ void client.ping().then(() => finish(), (error: Error) => finish(error));
56
+ };
57
+ const handleError = (error: Error) => finish(error);
58
+
59
+ client.once("ready", handleReady);
60
+ client.once("error", handleError);
61
+ if (client.status === "ready") handleReady();
62
+ });
63
+ }
64
+
65
+ export async function createRedisClient(): Promise<RedisClient> {
66
+ const redisUrl = process.env.REDIS_URL?.trim();
67
+ const nodeEnv = process.env.NODE_ENV?.trim() || "development";
68
+
69
+ const hasExternalRedisConfig = Boolean(
70
+ redisUrl ||
71
+ process.env.REDIS_HOST?.trim() ||
72
+ process.env.REDIS_PORT?.trim() ||
73
+ process.env.REDIS_PASSWORD?.trim(),
74
+ );
75
+
76
+ if (redisUrl === localRedisUrl && nodeEnv === "production") {
77
+ throw new Error(
78
+ "REDIS_URL=memory:// is for local development only; use redis:// or rediss:// in production",
79
+ );
80
+ }
81
+
82
+ const shouldUseMemoryRedis =
83
+ redisUrl === localRedisUrl ||
84
+ (!hasExternalRedisConfig && nodeEnv !== "production");
85
+
86
+ if (shouldUseMemoryRedis) {
87
+ const { default: RedisMock } = await import("ioredis-mock");
88
+ return new RedisMock() as unknown as RedisClient;
89
+ }
90
+
91
+ if (nodeEnv === "production" && !hasExternalRedisConfig) {
92
+ throw new Error(
93
+ "REDIS_URL=memory:// is for local development only; use redis:// or rediss:// in production",
94
+ );
95
+ }
96
+
97
+ if (redisUrl) {
98
+ return waitForRedis(new Redis(externalRedisUrl(redisUrl), redisOptions));
99
+ }
100
+
101
+ return waitForRedis(new Redis({
102
+ ...redisOptions,
103
+ host: process.env.REDIS_HOST?.trim() || "127.0.0.1",
104
+ port: redisPort(),
105
+ ...(process.env.REDIS_PASSWORD?.trim()
106
+ ? { password: process.env.REDIS_PASSWORD.trim() }
107
+ : {}),
108
+ }));
109
+ }
@@ -13,6 +13,10 @@ export const credentialsSchema = z.strictObject({
13
13
  password: passwordSchema,
14
14
  });
15
15
 
16
+ export const refreshTokenBodySchema = z.strictObject({
17
+ refreshToken: z.string().trim().min(16, "Refresh token is required").max(4096),
18
+ });
19
+
16
20
  const secretSchema = z.string().trim().min(32);
17
21
  const environmentValueSchema = z.string().trim().min(1);
18
22
  const portSchema = z.coerce.number().int().min(1).max(65535);
@@ -121,6 +125,14 @@ export function parseCredentials(body: unknown): z.infer<typeof credentialsSchem
121
125
  return result.data;
122
126
  }
123
127
 
128
+ export function parseRefreshToken(body: unknown): string {
129
+ const result = refreshTokenBodySchema.safeParse(body);
130
+ if (!result.success) {
131
+ throw new InputValidationError(validationMessage(result.error, "Refresh token is required"));
132
+ }
133
+ return result.data.refreshToken;
134
+ }
135
+
124
136
  export function parseIdentifier(value: unknown, label: string): string {
125
137
  const result = identifierSchema.safeParse(value);
126
138
  if (!result.success) throw new InputValidationError(`${label} is invalid`);
@@ -6,12 +6,21 @@ Generated by `create-authenik8-app`.
6
6
 
7
7
  ```bash
8
8
  npm install
9
- npm run docker:up
10
- npm run prisma:migrate
9
+ npm run db:migrate
11
10
  npm run dev
12
11
  ```
13
12
 
14
- For SQLite, Postgres in `docker-compose.yml` is optional. Redis is required for refresh-token security features.
13
+ The generated local environment uses an in-process Redis-compatible store, so Redis and Docker are optional during development. If you selected PostgreSQL, start it with `npm run docker:up` or point `DATABASE_URL` to an existing instance before migrating.
14
+
15
+ Validate the generated auth configuration and Redis connection at any time:
16
+
17
+ ```bash
18
+ npx create-authenik8-app@latest doctor
19
+ ```
20
+
21
+ `authenik8.json` records the generated architecture and Authenik8 engine version. It contains no secrets and should be committed with the project.
22
+
23
+ After committing the package-manager lockfile, run `npx create-authenik8-app@latest add ci-github` to add the pinned Doctor and upgrade-policy workflow. Preview it first with `--dry-run`.
15
24
 
16
25
  ## Environment
17
26
 
@@ -27,8 +36,7 @@ AUTHENIK8_ISSUER=http://localhost:3000
27
36
  AUTHENIK8_AUDIENCE=your-app-api
28
37
  AUTHENIK8_AGENTS={}
29
38
  REFRESH_SECRET=<generated-random-secret>
30
- REDIS_HOST=127.0.0.1
31
- REDIS_PORT=6379
39
+ REDIS_URL=memory://
32
40
  ```
33
41
 
34
42
  ## Routes
@@ -51,8 +59,7 @@ Authorization: Bearer <accessToken>
51
59
  Start the API in one terminal:
52
60
 
53
61
  ```bash
54
- npm run docker:up
55
- npm run prisma:migrate
62
+ npm run db:migrate
56
63
  npm run dev
57
64
  ```
58
65
 
@@ -78,8 +85,8 @@ Expected result: the route should reject the request because no access token was
78
85
  - `AUTHENIK8_ISSUER` / `AUTHENIK8_AUDIENCE`: required claims checked during verification.
79
86
  - `AUTHENIK8_AGENTS`: optional JSON map of agent IDs to exact `resource:action` scopes; keep `{}` to disable agent identity. See `AGENT_IDENTITY.md`.
80
87
  - `REFRESH_SECRET`: signs refresh tokens. Use a different long random value in production.
81
- - `REDIS_HOST`: Redis host for refresh-token/session security.
82
- - `REDIS_PORT`: Redis port, usually `6379` locally.
88
+ - `REDIS_URL`: use `memory://` for local development or `redis://` / `rediss://` for an external service. Production rejects `memory://`.
89
+ - `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD`: optional external Redis settings used when `REDIS_URL` is omitted.
83
90
 
84
91
  ## Frontend Fetch Example
85
92
 
@@ -101,9 +108,11 @@ export async function getProtected(accessToken: string) {
101
108
 
102
109
  ## Troubleshooting
103
110
 
104
- `Redis connection refused`: run `npm run docker:up` or start Redis locally with `redis-server --daemonize yes`.
111
+ `Redis connection refused`: verify `REDIS_URL`, or omit it and check `REDIS_HOST`, `REDIS_PORT`, and `REDIS_PASSWORD`.
112
+
113
+ `Prisma Client did not initialize`: run `npm run db:migrate`, then restart `npm run dev`.
105
114
 
106
- `Prisma Client did not initialize`: run `npm run prisma:migrate`, then restart `npm run dev`.
115
+ `REDIS_URL=memory:// is for local development only`: configure a `redis://` or `rediss://` endpoint before running in production.
107
116
 
108
117
  `AUTHENIK8_SIGNING_JWKS must be a valid JSON array`: restore the generated `.env` value or provide an ES256 P-256 key ring and matching `AUTHENIK8_ACTIVE_KID`.
109
118
 
@@ -117,4 +126,4 @@ Read `THREAT_MODEL.md` before deploying. It explains what Authenik8 protects, wh
117
126
 
118
127
  ## Deploy
119
128
 
120
- Use `npm run build`, run `npx prisma migrate deploy` for production databases, set real secrets in your host, and point Redis/Postgres env vars at managed services.
129
+ Use `npm run build`, run `npx prisma migrate deploy` for production databases, set `NODE_ENV=production`, store real secrets in your host, and point Redis/Postgres environment variables at managed services.
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "authenik8-app",
3
3
  "scripts": {
4
- "dev": "ts-node-dev --respawn --transpile-only ./src/server.ts",
4
+ "dev": "tsx watch ./src/server.ts",
5
5
  "build": "tsc",
6
6
  "start": "node dist/src/server.js",
7
- "prisma:migrate": "prisma migrate",
7
+ "db:migrate": "prisma db push && prisma generate",
8
+ "prisma:migrate": "prisma db push && prisma generate",
8
9
  "docker:up": "docker compose up -d --wait",
9
10
  "docker:down": "docker compose down"
10
11
  },
@@ -12,19 +13,21 @@
12
13
  "authenik8-core": "2.0.3",
13
14
  "dotenv": "^16.0.0",
14
15
  "express": "^4.18.2",
15
- "@prisma/client": "7.8.0",
16
+ "@prisma/client": "7.9.0",
17
+ "ioredis": "^5.8.1",
16
18
  "zod": "^4.4.3"
17
19
  },
18
20
  "devDependencies": {
19
21
  "@types/express": "^4.17.21",
20
22
  "@types/node": "^22.10.2",
21
- "ts-node-dev": "^2.0.0",
22
- "typescript": "^5.0.0",
23
- "prisma": "7.8.0"
24
-
23
+ "ioredis-mock": "8.13.1",
24
+ "prisma": "7.9.0",
25
+ "tsx": "^4.22.4",
26
+ "typescript": "^5.0.0"
25
27
  },
26
28
  "overrides": {
27
- "@hono/node-server": "1.19.13"
29
+ "find-my-way": "9.7.0",
30
+ "valibot": "1.4.2"
28
31
  },
29
32
  "type": "commonjs"
30
33
  }
@@ -5,4 +5,4 @@ AUTHENIK8_AUDIENCE=authenik8-app-api
5
5
  AUTHENIK8_AGENTS={}
6
6
  REFRESH_SECRET=replace-with-at-least-32-random-characters
7
7
  PORT=3000
8
- REDIS_URL=redis://localhost:6379
8
+ REDIS_URL=memory://
@@ -0,0 +1,109 @@
1
+ import Redis, { type Redis as RedisClient } from "ioredis";
2
+
3
+ const localRedisUrl = "memory://";
4
+ const redisOptions = {
5
+ connectTimeout: 5_000,
6
+ enableReadyCheck: true,
7
+ maxRetriesPerRequest: 2,
8
+ };
9
+
10
+ function externalRedisUrl(source: string): string {
11
+ let url: URL;
12
+ try {
13
+ url = new URL(source);
14
+ } catch {
15
+ throw new Error("REDIS_URL must be memory://, redis://, or rediss://");
16
+ }
17
+
18
+ if (url.protocol !== "redis:" && url.protocol !== "rediss:") {
19
+ throw new Error("REDIS_URL must be memory://, redis://, or rediss://");
20
+ }
21
+
22
+ return source;
23
+ }
24
+
25
+ function redisPort(): number {
26
+ const port = Number(process.env.REDIS_PORT?.trim() || "6379");
27
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
28
+ throw new Error("REDIS_PORT must be an integer from 1 to 65535");
29
+ }
30
+ return port;
31
+ }
32
+
33
+ function waitForRedis(client: RedisClient): Promise<RedisClient> {
34
+ return new Promise((resolve, reject) => {
35
+ let settled = false;
36
+ let readinessCheckStarted = false;
37
+ const cleanup = () => {
38
+ client.off("ready", handleReady);
39
+ client.off("error", handleError);
40
+ };
41
+ const finish = (error?: Error) => {
42
+ if (settled) return;
43
+ settled = true;
44
+ cleanup();
45
+ if (error) {
46
+ client.disconnect();
47
+ reject(error);
48
+ } else {
49
+ resolve(client);
50
+ }
51
+ };
52
+ const handleReady = () => {
53
+ if (readinessCheckStarted) return;
54
+ readinessCheckStarted = true;
55
+ void client.ping().then(() => finish(), (error: Error) => finish(error));
56
+ };
57
+ const handleError = (error: Error) => finish(error);
58
+
59
+ client.once("ready", handleReady);
60
+ client.once("error", handleError);
61
+ if (client.status === "ready") handleReady();
62
+ });
63
+ }
64
+
65
+ export async function createRedisClient(): Promise<RedisClient> {
66
+ const redisUrl = process.env.REDIS_URL?.trim();
67
+ const nodeEnv = process.env.NODE_ENV?.trim() || "development";
68
+
69
+ const hasExternalRedisConfig = Boolean(
70
+ redisUrl ||
71
+ process.env.REDIS_HOST?.trim() ||
72
+ process.env.REDIS_PORT?.trim() ||
73
+ process.env.REDIS_PASSWORD?.trim(),
74
+ );
75
+
76
+ if (redisUrl === localRedisUrl && nodeEnv === "production") {
77
+ throw new Error(
78
+ "REDIS_URL=memory:// is for local development only; use redis:// or rediss:// in production",
79
+ );
80
+ }
81
+
82
+ const shouldUseMemoryRedis =
83
+ redisUrl === localRedisUrl ||
84
+ (!hasExternalRedisConfig && nodeEnv !== "production");
85
+
86
+ if (shouldUseMemoryRedis) {
87
+ const { default: RedisMock } = await import("ioredis-mock");
88
+ return new RedisMock() as unknown as RedisClient;
89
+ }
90
+
91
+ if (nodeEnv === "production" && !hasExternalRedisConfig) {
92
+ throw new Error(
93
+ "REDIS_URL=memory:// is for local development only; use redis:// or rediss:// in production",
94
+ );
95
+ }
96
+
97
+ if (redisUrl) {
98
+ return waitForRedis(new Redis(externalRedisUrl(redisUrl), redisOptions));
99
+ }
100
+
101
+ return waitForRedis(new Redis({
102
+ ...redisOptions,
103
+ host: process.env.REDIS_HOST?.trim() || "127.0.0.1",
104
+ port: redisPort(),
105
+ ...(process.env.REDIS_PASSWORD?.trim()
106
+ ? { password: process.env.REDIS_PASSWORD.trim() }
107
+ : {}),
108
+ }));
109
+ }
@@ -1,6 +1,7 @@
1
1
  import dotenv from "dotenv";
2
2
  import { createAuthenik8 } from "authenik8-core";
3
3
  import { createApp } from "../app";
4
+ import { createRedisClient } from "./config/redis";
4
5
  import { agentIdentityConfig, authJwkConfig, requiredPort, requiredSecret } from "../utils/security";
5
6
 
6
7
  dotenv.config();
@@ -10,6 +11,7 @@ async function start() {
10
11
  jwt: authJwkConfig(),
11
12
  refreshSecret: requiredSecret("REFRESH_SECRET"),
12
13
  agent: agentIdentityConfig(),
14
+ redis: await createRedisClient(),
13
15
  });
14
16
 
15
17
  const app = createApp(auth);
@@ -1,8 +1,9 @@
1
1
  NODE_ENV=development
2
2
  PORT=3000
3
3
  WEB_ORIGIN=http://localhost:5173
4
- DATABASE_URL=postgresql://postgres:postgres@localhost:5432/authenik8?schema=public
5
- REDIS_URL=redis://localhost:6379
4
+ DATABASE_URL=postgresql://postgres:postgres@localhost:55432/authenik8?schema=public
5
+ REDIS_URL=memory://
6
+ AUTHENIK8_LOCAL_DATABASE=embedded
6
7
  AUTHENIK8_SIGNING_JWKS=
7
8
  AUTHENIK8_ACTIVE_KID=
8
9
  AUTHENIK8_ISSUER=http://localhost:3000
@@ -15,7 +15,8 @@ This document is the testable contract for the `fullstack` preset.
15
15
 
16
16
  | Command | Guarantee |
17
17
  | --- | --- |
18
- | `npm run dev` | Builds shared packages and starts API and web together. |
18
+ | `npm run dev` | Starts project-local PostgreSQL, prepares data, then starts API and web together. |
19
+ | `npm run setup` | Starts project-local PostgreSQL for the command, applies migrations, and creates idempotent seed data. |
19
20
  | `npm run build` | Produces the API and frontend production bundles. |
20
21
  | `npm start` | Starts Express, which serves `/api` and the built SPA. |
21
22
  | `npm run db:migrate` | Applies the Prisma PostgreSQL development migration. |
@@ -26,8 +27,11 @@ This document is the testable contract for the `fullstack` preset.
26
27
 
27
28
  - Vite: `http://localhost:5173`, proxying `/api` to Express.
28
29
  - Express: `http://localhost:3000`.
29
- - PostgreSQL: `localhost:5432` in local Docker Compose.
30
- - Redis: `localhost:6379` in local Docker Compose.
30
+ - PostgreSQL: project-local PGlite, bound to loopback only while local commands run.
31
+ - Redis: an in-process Redis-compatible store for local development.
32
+ - Docker Compose is optional and explicit through `npm run docker:up`.
33
+ - Set `AUTHENIK8_LOCAL_DATABASE=external` to use `DATABASE_URL` directly.
34
+ - Production requires external PostgreSQL and Redis services.
31
35
  - Production uses one origin. Express serves `apps/web/dist` after `npm run build`.
32
36
 
33
37
  ## Environment contract
@@ -8,18 +8,56 @@ agent access without exposing an unsafe public token-minting route.
8
8
 
9
9
  ## Local development
10
10
 
11
- Requirements: Node.js 20.19+, 22.12+, or 24+, npm, and Docker with Compose.
11
+ Requirements: Node.js 20.19+, 22.12+, or 24+ and npm. Docker is optional.
12
12
 
13
13
  ```bash
14
- docker compose up -d
15
- npm run db:migrate
16
- npm run db:seed
17
14
  npm run dev
18
15
  ```
19
16
 
20
- Open `http://localhost:5173`. The API runs on `http://localhost:3000/api`. The generated `.env` contains development values; replace every secret before deploying.
17
+ That one command starts project-local PostgreSQL, applies the shipped migration,
18
+ creates the seed administrator, and starts every development watcher.
19
+ Redis-compatible auth state runs inside the API process during local
20
+ development. Setup is idempotent, so subsequent runs reuse the same database. Open
21
+ `http://localhost:5173`. The API runs on `http://localhost:3000/api`.
21
22
 
22
- The seeded administrator uses `SEED_ADMIN_EMAIL` and `SEED_ADMIN_PASSWORD`. Change the default password immediately, including for local environments shared by multiple people.
23
+ Run setup without starting the watchers when needed:
24
+
25
+ ```bash
26
+ npm run setup
27
+ ```
28
+
29
+ The embedded PostgreSQL server listens only on loopback while a local command is
30
+ running and stores data under `.authenik8/`. No global PostgreSQL, Redis, or
31
+ Docker installation is required. If you provide your own PostgreSQL, set
32
+ `AUTHENIK8_LOCAL_DATABASE=external` and update `DATABASE_URL`.
33
+
34
+ Docker Compose remains available as an explicit option:
35
+
36
+ ```bash
37
+ npm run docker:up
38
+ ```
39
+
40
+ When using the containers, set `AUTHENIK8_LOCAL_DATABASE=external` and
41
+ `REDIS_URL=redis://localhost:56379`. Production must always use external
42
+ PostgreSQL and a real `redis://` or `rediss://` service. The API rejects the
43
+ in-process Redis setting in production.
44
+
45
+ The generated `.env` contains development values; replace every secret before deploying.
46
+
47
+ Validate the workspace auth configuration and selected Redis mode at any time:
48
+
49
+ ```bash
50
+ npx create-authenik8-app@latest doctor
51
+ ```
52
+
53
+ `authenik8.json` records the generated architecture and Authenik8 engine version. It contains no secrets and should be committed with the workspace.
54
+
55
+ After committing `package-lock.json`, run `npx create-authenik8-app@latest add ci-github` to add the pinned Doctor and upgrade-policy workflow. Preview it first with `--dry-run`.
56
+
57
+ With an unchanged generated `.env`, sign in with `admin@example.com` and
58
+ `ChangeMe123!`. These values come from `SEED_ADMIN_EMAIL` and
59
+ `SEED_ADMIN_PASSWORD`. Change the password immediately, including for local
60
+ environments shared by multiple people.
23
61
 
24
62
  ## Application map
25
63
 
@@ -62,4 +100,4 @@ Read [docs/PRODUCTION.md](docs/PRODUCTION.md) and [THREAT_MODEL.md](THREAT_MODEL
62
100
  - `GET /api/health/ready` checks PostgreSQL and Redis.
63
101
  - `GET /api/docs/openapi.json` returns the generated OpenAPI 3.1 contract.
64
102
  - `npm test` covers ownership/admin policies, encrypted cookies, origin and CSRF defenses, and the browser storage rule.
65
- - `npm run typecheck` checks each workspace.
103
+ - `npm run typecheck` builds internal packages, generates Prisma Client, and checks each workspace from a clean checkout.
@@ -11,16 +11,17 @@
11
11
  "build": "tsc -p tsconfig.json",
12
12
  "start": "node dist/server.js",
13
13
  "test": "vitest run",
14
+ "pretypecheck": "prisma generate",
14
15
  "typecheck": "tsc -p tsconfig.json --noEmit",
15
16
  "prisma:generate": "prisma generate",
16
- "prisma:migrate": "prisma migrate dev && prisma generate",
17
+ "prisma:migrate": "prisma migrate deploy && prisma generate",
17
18
  "prisma:seed": "prisma generate && prisma db seed",
18
19
  "prisma:studio": "prisma studio"
19
20
  },
20
21
  "dependencies": {
21
22
  "@authenik8/contracts": "0.1.0",
22
- "@prisma/adapter-pg": "7.8.0",
23
- "@prisma/client": "7.8.0",
23
+ "@prisma/adapter-pg": "7.9.0",
24
+ "@prisma/client": "7.9.0",
24
25
  "authenik8-core": "2.0.3",
25
26
  "bcryptjs": "^2.4.3",
26
27
  "cookie-parser": "^1.4.7",
@@ -28,6 +29,7 @@
28
29
  "dotenv": "^17.3.1",
29
30
  "express": "^5.2.1",
30
31
  "ioredis": "^5.10.0",
32
+ "ioredis-mock": "8.13.1",
31
33
  "pino": "^9.7.0",
32
34
  "pino-http": "^10.4.0",
33
35
  "zod": "^4.4.3"
@@ -39,7 +41,7 @@
39
41
  "@types/express": "^5.0.6",
40
42
  "@types/node": "^25.0.0",
41
43
  "@types/supertest": "^7.2.0",
42
- "prisma": "7.8.0",
44
+ "prisma": "7.9.0",
43
45
  "supertest": "^7.2.2",
44
46
  "tsx": "^4.22.4",
45
47
  "typescript": "^6.0.3",