@techstream/quark-create-app 1.5.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/package.json +4 -2
  2. package/src/index.js +62 -14
  3. package/templates/base-project/.github/dependabot.yml +12 -0
  4. package/templates/base-project/.github/workflows/ci.yml +97 -0
  5. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  6. package/templates/base-project/.github/workflows/release.yml +38 -0
  7. package/templates/base-project/apps/web/biome.json +7 -0
  8. package/templates/base-project/apps/web/jsconfig.json +5 -5
  9. package/templates/base-project/apps/web/next.config.js +90 -1
  10. package/templates/base-project/apps/web/package.json +7 -7
  11. package/templates/base-project/apps/web/railway.json +15 -0
  12. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +6 -7
  13. package/templates/base-project/apps/web/src/app/layout.js +3 -4
  14. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  15. package/templates/base-project/apps/web/src/app/robots.js +21 -0
  16. package/templates/base-project/apps/web/src/app/sitemap.js +20 -0
  17. package/templates/base-project/apps/web/src/lib/seo/indexing.js +23 -0
  18. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +33 -0
  19. package/templates/base-project/apps/web/src/proxy.js +1 -2
  20. package/templates/base-project/apps/worker/package.json +5 -5
  21. package/templates/base-project/apps/worker/railway.json +13 -0
  22. package/templates/base-project/apps/worker/src/index.js +30 -12
  23. package/templates/base-project/apps/worker/src/index.test.js +296 -15
  24. package/templates/base-project/biome.json +44 -0
  25. package/templates/base-project/docker-compose.yml +7 -4
  26. package/templates/base-project/package.json +1 -1
  27. package/templates/base-project/packages/db/package.json +1 -1
  28. package/templates/base-project/packages/db/prisma/schema.prisma +1 -17
  29. package/templates/base-project/packages/db/prisma.config.ts +8 -10
  30. package/templates/base-project/packages/db/scripts/seed.js +117 -30
  31. package/templates/base-project/packages/db/src/client.js +1 -18
  32. package/templates/base-project/packages/db/src/connection.js +44 -0
  33. package/templates/base-project/packages/db/src/connection.test.js +119 -0
  34. package/templates/base-project/packages/db/src/queries.js +52 -118
  35. package/templates/base-project/packages/db/src/queries.test.js +0 -29
  36. package/templates/base-project/packages/db/src/schemas.js +0 -12
  37. package/templates/base-project/pnpm-workspace.yaml +4 -0
  38. package/templates/base-project/turbo.json +5 -3
  39. package/templates/config/package.json +2 -0
  40. package/templates/config/src/environment.js +270 -0
  41. package/templates/config/src/index.js +13 -18
  42. package/templates/config/src/load-config.js +135 -0
  43. package/templates/config/src/validate-env.js +123 -16
  44. package/templates/jobs/package.json +2 -2
  45. package/templates/jobs/src/definitions.test.js +34 -0
  46. package/templates/jobs/src/index.js +1 -1
  47. package/templates/ui/package.json +4 -4
  48. package/templates/ui/src/button.test.js +23 -0
  49. package/templates/ui/src/index.js +1 -3
  50. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +0 -65
  51. package/templates/base-project/apps/web/src/app/api/posts/route.js +0 -95
  52. package/templates/ui/src/card.js +0 -14
  53. package/templates/ui/src/input.js +0 -11
@@ -0,0 +1,270 @@
1
+ /**
2
+ * @techstream/quark-config - Environment Configuration
3
+ * Provides environment-specific defaults for dev, test, staging, and production.
4
+ * Each environment defines sensible defaults that can be overridden via env vars.
5
+ */
6
+
7
+ /**
8
+ * @typedef {"development" | "test" | "staging" | "production"} Environment
9
+ *
10
+ * @typedef {Object} EnvironmentConfig
11
+ * @property {Environment} environment - Resolved environment name
12
+ * @property {boolean} isProduction - True in production and staging
13
+ * @property {boolean} isDevelopment - True in development
14
+ * @property {boolean} isTest - True in test
15
+ * @property {Object} server - Server configuration
16
+ * @property {number} server.port - HTTP port
17
+ * @property {Object} rateLimit - Rate limiting defaults
18
+ * @property {number} rateLimit.windowMs - Rate limit window in ms
19
+ * @property {number} rateLimit.maxRequests - Max requests per window
20
+ * @property {number} rateLimit.authMaxRequests - Max auth requests per window
21
+ * @property {Object} cache - Cache configuration
22
+ * @property {number} cache.defaultTtl - Default cache TTL in seconds
23
+ * @property {Object} logging - Logging configuration
24
+ * @property {string} logging.level - Minimum log level
25
+ * @property {boolean} logging.json - Use JSON format
26
+ * @property {Object} db - Database configuration
27
+ * @property {number} db.poolMax - Max DB pool connections
28
+ * @property {number} db.poolIdleTimeout - Idle timeout in seconds
29
+ * @property {number} db.connectionTimeout - Connection timeout in seconds
30
+ * @property {Object} email - Email configuration
31
+ * @property {number} email.timeout - SMTP/API timeout in ms
32
+ * @property {Object} security - Security configuration
33
+ * @property {boolean} security.enforceHttps - Require HTTPS
34
+ * @property {boolean} security.trustProxy - Trust proxy headers
35
+ * @property {Object} features - Feature flags
36
+ * @property {boolean} features.debugRoutes - Enable debug endpoints
37
+ * @property {boolean} features.seedOnStart - Auto-seed database on startup
38
+ * @property {boolean} features.detailedErrors - Include stack traces in error responses
39
+ */
40
+
41
+ /** @type {Record<Environment, EnvironmentConfig>} */
42
+ const ENVIRONMENT_CONFIGS = {
43
+ development: {
44
+ environment: "development",
45
+ isProduction: false,
46
+ isDevelopment: true,
47
+ isTest: false,
48
+ server: {
49
+ port: 3000,
50
+ },
51
+ rateLimit: {
52
+ windowMs: 15 * 60 * 1000,
53
+ maxRequests: 1000,
54
+ authMaxRequests: 50,
55
+ },
56
+ cache: {
57
+ defaultTtl: 60,
58
+ },
59
+ logging: {
60
+ level: "debug",
61
+ json: false,
62
+ },
63
+ db: {
64
+ poolMax: 5,
65
+ poolIdleTimeout: 30,
66
+ connectionTimeout: 5,
67
+ },
68
+ email: {
69
+ timeout: 10_000,
70
+ },
71
+ security: {
72
+ enforceHttps: false,
73
+ trustProxy: false,
74
+ },
75
+ features: {
76
+ debugRoutes: true,
77
+ seedOnStart: false,
78
+ detailedErrors: true,
79
+ },
80
+ },
81
+
82
+ test: {
83
+ environment: "test",
84
+ isProduction: false,
85
+ isDevelopment: false,
86
+ isTest: true,
87
+ server: {
88
+ port: 3001,
89
+ },
90
+ rateLimit: {
91
+ windowMs: 15 * 60 * 1000,
92
+ maxRequests: 10_000,
93
+ authMaxRequests: 10_000,
94
+ },
95
+ cache: {
96
+ defaultTtl: 0,
97
+ },
98
+ logging: {
99
+ level: "warn",
100
+ json: false,
101
+ },
102
+ db: {
103
+ poolMax: 3,
104
+ poolIdleTimeout: 10,
105
+ connectionTimeout: 5,
106
+ },
107
+ email: {
108
+ timeout: 5_000,
109
+ },
110
+ security: {
111
+ enforceHttps: false,
112
+ trustProxy: false,
113
+ },
114
+ features: {
115
+ debugRoutes: true,
116
+ seedOnStart: false,
117
+ detailedErrors: true,
118
+ },
119
+ },
120
+
121
+ staging: {
122
+ environment: "staging",
123
+ isProduction: true,
124
+ isDevelopment: false,
125
+ isTest: false,
126
+ server: {
127
+ port: 3000,
128
+ },
129
+ rateLimit: {
130
+ windowMs: 15 * 60 * 1000,
131
+ maxRequests: 100,
132
+ authMaxRequests: 5,
133
+ },
134
+ cache: {
135
+ defaultTtl: 300,
136
+ },
137
+ logging: {
138
+ level: "info",
139
+ json: true,
140
+ },
141
+ db: {
142
+ poolMax: 10,
143
+ poolIdleTimeout: 30,
144
+ connectionTimeout: 5,
145
+ },
146
+ email: {
147
+ timeout: 10_000,
148
+ },
149
+ security: {
150
+ enforceHttps: true,
151
+ trustProxy: true,
152
+ },
153
+ features: {
154
+ debugRoutes: false,
155
+ seedOnStart: false,
156
+ detailedErrors: false,
157
+ },
158
+ },
159
+
160
+ production: {
161
+ environment: "production",
162
+ isProduction: true,
163
+ isDevelopment: false,
164
+ isTest: false,
165
+ server: {
166
+ port: 3000,
167
+ },
168
+ rateLimit: {
169
+ windowMs: 15 * 60 * 1000,
170
+ maxRequests: 100,
171
+ authMaxRequests: 5,
172
+ },
173
+ cache: {
174
+ defaultTtl: 600,
175
+ },
176
+ logging: {
177
+ level: "info",
178
+ json: true,
179
+ },
180
+ db: {
181
+ poolMax: 10,
182
+ poolIdleTimeout: 30,
183
+ connectionTimeout: 5,
184
+ },
185
+ email: {
186
+ timeout: 10_000,
187
+ },
188
+ security: {
189
+ enforceHttps: true,
190
+ trustProxy: true,
191
+ },
192
+ features: {
193
+ debugRoutes: false,
194
+ seedOnStart: false,
195
+ detailedErrors: false,
196
+ },
197
+ },
198
+ };
199
+
200
+ /** Valid environment names */
201
+ export const ENVIRONMENTS = /** @type {const} */ ([
202
+ "development",
203
+ "test",
204
+ "staging",
205
+ "production",
206
+ ]);
207
+
208
+ /**
209
+ * Resolves the current environment from NODE_ENV.
210
+ * Maps common aliases (e.g. "dev" → "development", "prod" → "production").
211
+ * Defaults to "development" if unset or unrecognized.
212
+ *
213
+ * @param {string} [nodeEnv] - Override for NODE_ENV (defaults to process.env.NODE_ENV)
214
+ * @returns {Environment}
215
+ */
216
+ export function resolveEnvironment(nodeEnv) {
217
+ const raw = (nodeEnv ?? process.env.NODE_ENV ?? "").toLowerCase().trim();
218
+
219
+ const aliases = {
220
+ dev: "development",
221
+ development: "development",
222
+ test: "test",
223
+ testing: "test",
224
+ staging: "staging",
225
+ stage: "staging",
226
+ prod: "production",
227
+ production: "production",
228
+ };
229
+
230
+ return aliases[raw] || "development";
231
+ }
232
+
233
+ /**
234
+ * Returns the full environment configuration for a given environment.
235
+ * Unknown environments fall back to development.
236
+ *
237
+ * @param {string} [nodeEnv] - Override for NODE_ENV
238
+ * @returns {EnvironmentConfig}
239
+ */
240
+ export function getEnvironmentConfig(nodeEnv) {
241
+ const env = resolveEnvironment(nodeEnv);
242
+ return { ...ENVIRONMENT_CONFIGS[env] };
243
+ }
244
+
245
+ /**
246
+ * Shallow-merges environment config with user-provided overrides (one level deep).
247
+ * Top-level scalars are replaced; top-level objects are spread-merged.
248
+ * Useful when downstream apps need to adjust defaults per-environment.
249
+ *
250
+ * @param {EnvironmentConfig} base - Base environment config
251
+ * @param {Record<string, unknown>} overrides - Partial overrides to merge
252
+ * @returns {EnvironmentConfig}
253
+ */
254
+ export function mergeConfig(base, overrides) {
255
+ const result = { ...base };
256
+ for (const [key, value] of Object.entries(overrides)) {
257
+ if (
258
+ value != null &&
259
+ typeof value === "object" &&
260
+ !Array.isArray(value) &&
261
+ typeof result[key] === "object" &&
262
+ result[key] != null
263
+ ) {
264
+ result[key] = { ...result[key], ...value };
265
+ } else {
266
+ result[key] = value;
267
+ }
268
+ }
269
+ return result;
270
+ }
@@ -1,21 +1,16 @@
1
1
  export const config = {
2
- appName: "My Quark App",
3
- environment: process.env.NODE_ENV || "development",
4
- api: {
5
- baseUrl: process.env.API_BASE_URL || "http://localhost:3000",
6
- },
7
- database: {
8
- url:
9
- process.env.DATABASE_URL ||
10
- "postgresql://user:password@localhost:5432/myapp",
11
- },
12
- redis: {
13
- url: process.env.REDIS_URL || "redis://localhost:6379",
14
- },
15
- email: {
16
- from: process.env.EMAIL_FROM || "noreply@myquarkapp.com",
17
- provider: process.env.EMAIL_PROVIDER || "smtp",
18
- },
2
+ appName: process.env.APP_NAME || "Quark",
3
+ appDescription:
4
+ process.env.APP_DESCRIPTION ||
5
+ "A modern monorepo with Next.js, React, and Prisma",
19
6
  };
20
7
 
21
- export default config;
8
+ export { getAllowedOrigins, getAppUrl, syncNextAuthUrl } from "./app-url.js";
9
+ export {
10
+ ENVIRONMENTS,
11
+ getEnvironmentConfig,
12
+ mergeConfig,
13
+ resolveEnvironment,
14
+ } from "./environment.js";
15
+ export { getConfig, loadConfig, resetConfig } from "./load-config.js";
16
+ export { loadEnv, validateEnv } from "./validate-env.js";
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @techstream/quark-config - Configuration Loader
3
+ * Centralised configuration management that combines environment validation,
4
+ * environment-specific defaults, and user overrides into a single config object.
5
+ *
6
+ * Usage:
7
+ * import { loadConfig } from "@techstream/quark-config";
8
+ * const config = loadConfig(); // validates env + returns typed config
9
+ * const config = loadConfig({ cache: { defaultTtl: 120 } }); // with overrides
10
+ */
11
+
12
+ import { getAllowedOrigins, getAppUrl } from "./app-url.js";
13
+ import { getEnvironmentConfig, mergeConfig } from "./environment.js";
14
+ import { validateEnv } from "./validate-env.js";
15
+
16
+ /** @type {import("./environment.js").EnvironmentConfig | null} */
17
+ let cachedConfig = null;
18
+
19
+ /**
20
+ * Loads the full application configuration.
21
+ *
22
+ * 1. Validates all required environment variables (throws on failure).
23
+ * 2. Resolves the current environment (dev/test/staging/production).
24
+ * 3. Merges environment-specific defaults with any user overrides.
25
+ * 4. Enriches with computed values (APP_URL, allowed origins, ports).
26
+ * 5. Caches the result — subsequent calls return the same object.
27
+ *
28
+ * @param {Record<string, unknown>} [overrides] - Optional partial overrides
29
+ * @param {Object} [options]
30
+ * @param {boolean} [options.fresh=false] - Force re-computation (bypass cache)
31
+ * @returns {import("./environment.js").EnvironmentConfig & { appUrl: string, allowedOrigins: string[], validated: Record<string, string> }}
32
+ */
33
+ export function loadConfig(overrides = {}, options = {}) {
34
+ if (cachedConfig && !options.fresh) {
35
+ return cachedConfig;
36
+ }
37
+
38
+ // Step 1: Validate environment variables
39
+ const { validated } = validateEnv();
40
+
41
+ // Step 2: Resolve environment and get defaults
42
+ const envConfig = getEnvironmentConfig();
43
+
44
+ // Step 3: Apply env-var driven overrides
45
+ const envOverrides = buildEnvOverrides(envConfig);
46
+
47
+ // Step 4: Merge: env defaults → env-var overrides → user overrides
48
+ let config = mergeConfig(envConfig, envOverrides);
49
+ config = mergeConfig(config, overrides);
50
+
51
+ // Step 5: Attach computed values
52
+ config.appUrl = getAppUrl();
53
+ config.allowedOrigins = getAllowedOrigins();
54
+ config.validated = validated;
55
+
56
+ cachedConfig = config;
57
+ return config;
58
+ }
59
+
60
+ /**
61
+ * Clears the cached configuration. Useful in tests.
62
+ */
63
+ export function resetConfig() {
64
+ cachedConfig = null;
65
+ }
66
+
67
+ /**
68
+ * Returns the cached configuration if already loaded, otherwise null.
69
+ * Does NOT trigger validation — use `loadConfig()` for that.
70
+ * @returns {ReturnType<typeof loadConfig> | null}
71
+ */
72
+ export function getConfig() {
73
+ return cachedConfig;
74
+ }
75
+
76
+ /**
77
+ * Reads specific environment variables and converts them to config overrides.
78
+ * This allows env vars to take precedence over environment defaults.
79
+ *
80
+ * @param {import("./environment.js").EnvironmentConfig} envConfig
81
+ * @returns {Record<string, unknown>}
82
+ */
83
+ function buildEnvOverrides(_envConfig) {
84
+ const overrides = {};
85
+
86
+ // Server
87
+ if (process.env.PORT) {
88
+ const port = Number.parseInt(process.env.PORT, 10);
89
+ if (!Number.isNaN(port)) overrides.server = { port };
90
+ }
91
+
92
+ // Rate limiting
93
+ if (process.env.RATE_LIMIT_MAX) {
94
+ const maxRequests = Number.parseInt(process.env.RATE_LIMIT_MAX, 10);
95
+ if (!Number.isNaN(maxRequests)) {
96
+ overrides.rateLimit = { ...overrides.rateLimit, maxRequests };
97
+ }
98
+ }
99
+ if (process.env.RATE_LIMIT_WINDOW_MS) {
100
+ const windowMs = Number.parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10);
101
+ if (!Number.isNaN(windowMs)) {
102
+ overrides.rateLimit = { ...overrides.rateLimit, windowMs };
103
+ }
104
+ }
105
+
106
+ // Cache
107
+ if (process.env.CACHE_TTL) {
108
+ const defaultTtl = Number.parseInt(process.env.CACHE_TTL, 10);
109
+ if (!Number.isNaN(defaultTtl)) overrides.cache = { defaultTtl };
110
+ }
111
+
112
+ // Logging
113
+ if (process.env.LOG_LEVEL) {
114
+ overrides.logging = { ...overrides.logging, level: process.env.LOG_LEVEL };
115
+ }
116
+
117
+ // Database pool
118
+ if (process.env.DB_POOL_MAX) {
119
+ const poolMax = Number.parseInt(process.env.DB_POOL_MAX, 10);
120
+ if (!Number.isNaN(poolMax)) {
121
+ overrides.db = { ...overrides.db, poolMax };
122
+ }
123
+ }
124
+ if (process.env.DB_POOL_IDLE_TIMEOUT) {
125
+ const poolIdleTimeout = Number.parseInt(
126
+ process.env.DB_POOL_IDLE_TIMEOUT,
127
+ 10,
128
+ );
129
+ if (!Number.isNaN(poolIdleTimeout)) {
130
+ overrides.db = { ...overrides.db, poolIdleTimeout };
131
+ }
132
+ }
133
+
134
+ return overrides;
135
+ }
@@ -22,16 +22,31 @@ const envSchema = {
22
22
  REDIS_HOST: { required: false, description: "Redis host" },
23
23
  REDIS_PORT: { required: false, description: "Redis port" },
24
24
 
25
- // Mail (local SMTP server)
26
- MAIL_SMTP_URL: { required: false, description: "Mail SMTP URL" },
25
+ // Mail (local SMTP — Mailpit in dev)
27
26
  MAIL_HOST: { required: false, description: "Mail host" },
28
27
  MAIL_SMTP_PORT: { required: false, description: "Mail SMTP port" },
29
28
  MAIL_UI_PORT: { required: false, description: "Mail UI port" },
30
29
 
30
+ // Production SMTP (used when SMTP_HOST is set)
31
+ SMTP_HOST: { required: false, description: "Production SMTP host" },
32
+ SMTP_PORT: { required: false, description: "Production SMTP port" },
33
+ SMTP_SECURE: { required: false, description: "Use TLS for SMTP" },
34
+ SMTP_USER: { required: false, description: "SMTP username" },
35
+ SMTP_PASSWORD: { required: false, description: "SMTP password" },
36
+
37
+ // Email provider
38
+ EMAIL_PROVIDER: {
39
+ required: false,
40
+ description: 'Email provider — "smtp" (default) or "resend"',
41
+ },
42
+ EMAIL_FROM: { required: false, description: "Sender email address" },
43
+ RESEND_API_KEY: { required: false, description: "Resend API key" },
44
+
31
45
  // NextAuth
32
46
  NEXTAUTH_SECRET: {
33
47
  required: true,
34
48
  description: "NextAuth secret for JWT signing",
49
+ minLength: 32,
35
50
  },
36
51
  NEXTAUTH_URL: {
37
52
  required: false,
@@ -39,6 +54,10 @@ const envSchema = {
39
54
  },
40
55
 
41
56
  // Application
57
+ APP_NAME: {
58
+ required: false,
59
+ description: "Application name — used in metadata, emails, and page titles",
60
+ },
42
61
  APP_URL: {
43
62
  required: false,
44
63
  description:
@@ -46,57 +65,145 @@ const envSchema = {
46
65
  },
47
66
  NODE_ENV: {
48
67
  required: false,
49
- description: "Environment (development, test, production)",
68
+ description: "Environment (development, test, staging, production)",
50
69
  },
51
70
  PORT: { required: false, description: "Web server port" },
71
+
72
+ // Storage
73
+ STORAGE_PROVIDER: {
74
+ required: false,
75
+ description: 'Storage provider — "local" (default) or "s3"',
76
+ },
77
+ STORAGE_LOCAL_DIR: {
78
+ required: false,
79
+ description: "Local storage directory",
80
+ },
81
+ S3_BUCKET: { required: false, description: "S3 bucket name" },
82
+ S3_REGION: { required: false, description: "S3 region" },
83
+ S3_ENDPOINT: { required: false, description: "S3-compatible endpoint URL" },
84
+ S3_ACCESS_KEY_ID: { required: false, description: "S3 access key" },
85
+ S3_SECRET_ACCESS_KEY: { required: false, description: "S3 secret key" },
86
+ S3_PUBLIC_URL: { required: false, description: "S3 public URL prefix" },
52
87
  };
53
88
 
54
89
  /**
55
- * Validates environment variables against schema
90
+ * Validates environment variables against schema.
91
+ *
92
+ * @param {"web" | "worker"} [service="web"] — The service being validated.
93
+ * Worker skips web-only checks (e.g. NEXTAUTH_SECRET).
56
94
  * @throws {Error} If required environment variables are missing
57
- * @returns {Object} Validated environment object
95
+ * @returns {{ validated: Object, warnings: string[] }}
58
96
  */
59
- export function validateEnv() {
97
+ export function validateEnv(service = "web") {
60
98
  const errors = [];
99
+ const warnings = [];
61
100
  const validated = {};
101
+ const isTest = process.env.NODE_ENV === "test";
102
+
103
+ // Web-only required fields that workers can skip
104
+ const webOnlyRequired = new Set(["NEXTAUTH_SECRET"]);
62
105
 
63
106
  for (const [key, config] of Object.entries(envSchema)) {
64
107
  const value = process.env[key];
65
108
 
66
- if (config.required && !value) {
109
+ // Skip web-only required checks for worker service
110
+ const isRequired =
111
+ config.required && !(service === "worker" && webOnlyRequired.has(key));
112
+
113
+ if (isRequired && !value) {
67
114
  errors.push(
68
115
  `Missing required environment variable: ${key} (${config.description})`,
69
116
  );
70
117
  }
71
118
 
119
+ if (value && config.minLength && value.length < config.minLength) {
120
+ errors.push(
121
+ `${key} must be at least ${config.minLength} characters (${config.description})`,
122
+ );
123
+ }
124
+
72
125
  if (value) {
73
126
  validated[key] = value;
74
127
  }
75
128
  }
76
129
 
130
+ // --- Cross-field validation ---
131
+
132
+ // Database: either DATABASE_URL or POSTGRES_USER must be set (skip in test)
133
+ if (!isTest) {
134
+ const hasDbUrl = !!process.env.DATABASE_URL;
135
+ const hasPostgresUser = !!process.env.POSTGRES_USER;
136
+ if (!hasDbUrl && !hasPostgresUser) {
137
+ errors.push(
138
+ "Database not configured: set DATABASE_URL or POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB",
139
+ );
140
+ }
141
+ }
142
+
143
+ // Redis: warn if not configured (defaults to localhost in dev, will fail in prod)
144
+ if (
145
+ !process.env.REDIS_URL &&
146
+ !process.env.REDIS_HOST &&
147
+ process.env.NODE_ENV === "production"
148
+ ) {
149
+ warnings.push(
150
+ "Redis not configured: set REDIS_URL or REDIS_HOST (defaults to localhost)",
151
+ );
152
+ }
153
+
154
+ // Conditional: S3 storage requires bucket + credentials
155
+ if (process.env.STORAGE_PROVIDER === "s3") {
156
+ for (const key of [
157
+ "S3_BUCKET",
158
+ "S3_ACCESS_KEY_ID",
159
+ "S3_SECRET_ACCESS_KEY",
160
+ ]) {
161
+ if (!process.env[key]) {
162
+ errors.push(
163
+ `Missing ${key} — required when STORAGE_PROVIDER=s3 (${envSchema[key].description})`,
164
+ );
165
+ }
166
+ }
167
+ }
168
+
169
+ // Conditional: Resend provider requires API key
170
+ if (process.env.EMAIL_PROVIDER === "resend" && !process.env.RESEND_API_KEY) {
171
+ errors.push("Missing RESEND_API_KEY — required when EMAIL_PROVIDER=resend");
172
+ }
173
+
174
+ // Log warnings (non-fatal)
175
+ for (const warning of warnings) {
176
+ console.warn(`[env] ⚠️ ${warning}`);
177
+ }
178
+
77
179
  if (errors.length > 0) {
78
180
  const errorMessage = `Environment Validation Failed:\n${errors.join("\n")}`;
79
181
  throw new Error(errorMessage);
80
182
  }
81
183
 
82
184
  // Ensure NEXTAUTH_URL is derived from APP_URL when not explicitly set
83
- syncNextAuthUrl();
185
+ if (service === "web") {
186
+ syncNextAuthUrl();
84
187
 
85
- // Include the (possibly derived) NEXTAUTH_URL in the validated object
86
- if (process.env.NEXTAUTH_URL && !validated.NEXTAUTH_URL) {
87
- validated.NEXTAUTH_URL = process.env.NEXTAUTH_URL;
188
+ // Include the (possibly derived) NEXTAUTH_URL in the validated object
189
+ if (process.env.NEXTAUTH_URL && !validated.NEXTAUTH_URL) {
190
+ validated.NEXTAUTH_URL = process.env.NEXTAUTH_URL;
191
+ }
88
192
  }
89
193
 
90
- return validated;
194
+ return { validated, warnings };
91
195
  }
92
196
 
93
197
  /**
94
- * Loads and validates environment variables
95
- * Call this function at application startup
198
+ * Loads and validates environment variables.
199
+ * Call this function at application startup.
200
+ *
201
+ * @param {"web" | "worker"} [service="web"]
96
202
  */
97
- export function loadEnv() {
203
+ export function loadEnv(service = "web") {
98
204
  try {
99
- return validateEnv();
205
+ const { validated } = validateEnv(service);
206
+ return validated;
100
207
  } catch (error) {
101
208
  console.error(error.message);
102
209
  process.exit(1);
@@ -2,7 +2,7 @@
2
2
  "name": "@myquark/jobs",
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
- "exports": {
6
- ".": "./src/index.js"
5
+ "dependencies": {
6
+ "bullmq": "^5.69.3"
7
7
  }
8
8
  }
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert";
2
+ import { test } from "node:test";
3
+ import { JOB_NAMES, JOB_QUEUES } from "./definitions.js";
4
+
5
+ test("Job Definitions - defines email queue", () => {
6
+ assert.strictEqual(JOB_QUEUES.EMAIL, "email-queue");
7
+ });
8
+
9
+ test("Job Definitions - defines files queue", () => {
10
+ assert.strictEqual(JOB_QUEUES.FILES, "files-queue");
11
+ });
12
+
13
+ test("Job Definitions - defines welcome email job name", () => {
14
+ assert.strictEqual(JOB_NAMES.SEND_WELCOME_EMAIL, "send-welcome-email");
15
+ });
16
+
17
+ test("Job Definitions - defines reset password email job name", () => {
18
+ assert.strictEqual(
19
+ JOB_NAMES.SEND_RESET_PASSWORD_EMAIL,
20
+ "send-reset-password-email",
21
+ );
22
+ });
23
+
24
+ test("Job Definitions - defines cleanup orphaned files job name", () => {
25
+ assert.strictEqual(
26
+ JOB_NAMES.CLEANUP_ORPHANED_FILES,
27
+ "cleanup-orphaned-files",
28
+ );
29
+ });
30
+
31
+ test("Job Definitions - queues are readonly", () => {
32
+ assert.strictEqual(typeof JOB_QUEUES.EMAIL, "string");
33
+ assert.strictEqual(typeof JOB_QUEUES.FILES, "string");
34
+ });
@@ -1 +1 @@
1
- export { JOB_NAMES, JOB_QUEUES } from "./definitions.js";
1
+ export * from "./definitions.js";