@usequark/quark-create-app 1.23.11
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 +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { createLogger } from "@techstream/quark-core";
|
|
2
|
+
import { ValidationError } from "@techstream/quark-core/errors";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { syncNextAuthUrl } from "./app-url.js";
|
|
5
|
+
|
|
6
|
+
const log = createLogger("env");
|
|
7
|
+
|
|
8
|
+
function getResolvedNextAuthSecret() {
|
|
9
|
+
return process.env.NEXTAUTH_SECRET || process.env.AUTH_SECRET || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const BOOLEAN_LIKE_VALUES = new Set([
|
|
13
|
+
"true",
|
|
14
|
+
"false",
|
|
15
|
+
"1",
|
|
16
|
+
"0",
|
|
17
|
+
"yes",
|
|
18
|
+
"no",
|
|
19
|
+
"on",
|
|
20
|
+
"off",
|
|
21
|
+
]);
|
|
22
|
+
const TRUTHY_VALUES = new Set(["true", "1", "yes", "on"]);
|
|
23
|
+
const UUID_PATTERN =
|
|
24
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
25
|
+
|
|
26
|
+
function isBooleanLike(value) {
|
|
27
|
+
return BOOLEAN_LIKE_VALUES.has(value.trim().toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isTruthyLike(value) {
|
|
31
|
+
return TRUTHY_VALUES.has(value.trim().toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isAbsoluteHttpUrl(value) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = new URL(value);
|
|
37
|
+
return ["http:", "https:"].includes(parsed.protocol);
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isUuid(value) {
|
|
44
|
+
return UUID_PATTERN.test(value.trim());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Environment variable field descriptions (used in error messages).
|
|
49
|
+
*/
|
|
50
|
+
const ENV_DESCRIPTIONS = {
|
|
51
|
+
// Database
|
|
52
|
+
DATABASE_URL: "PostgreSQL connection string",
|
|
53
|
+
POSTGRES_HOST: "PostgreSQL host",
|
|
54
|
+
POSTGRES_PORT: "PostgreSQL port",
|
|
55
|
+
POSTGRES_USER: "PostgreSQL user",
|
|
56
|
+
POSTGRES_PASSWORD: "PostgreSQL password",
|
|
57
|
+
POSTGRES_DB: "PostgreSQL database name",
|
|
58
|
+
|
|
59
|
+
// Redis
|
|
60
|
+
REDIS_URL: "Redis connection string",
|
|
61
|
+
REDIS_HOST: "Redis host",
|
|
62
|
+
REDIS_PORT: "Redis port",
|
|
63
|
+
|
|
64
|
+
// Mail (local SMTP - Mailpit in dev)
|
|
65
|
+
MAIL_HOST: "Mail host",
|
|
66
|
+
MAIL_SMTP_PORT: "Mail SMTP port",
|
|
67
|
+
MAIL_UI_PORT: "Mail UI port",
|
|
68
|
+
|
|
69
|
+
// Production SMTP (used when SMTP_HOST is set)
|
|
70
|
+
SMTP_HOST: "Production SMTP host",
|
|
71
|
+
SMTP_PORT: "Production SMTP port",
|
|
72
|
+
SMTP_SECURE: "Use TLS for SMTP",
|
|
73
|
+
SMTP_USER: "SMTP username",
|
|
74
|
+
SMTP_PASSWORD: "SMTP password",
|
|
75
|
+
|
|
76
|
+
// Email provider
|
|
77
|
+
EMAIL_PROVIDER: 'Email provider - "smtp" (default), "resend", or "zeptomail"',
|
|
78
|
+
EMAIL_FROM: "Sender email address",
|
|
79
|
+
RESEND_API_KEY: "Resend API key",
|
|
80
|
+
ZEPTOMAIL_TOKEN: "Zeptomail API token",
|
|
81
|
+
ZEPTOMAIL_URL: "Zeptomail API base URL",
|
|
82
|
+
ZEPTOMAIL_BOUNCE_EMAIL: "Zeptomail bounce email address",
|
|
83
|
+
|
|
84
|
+
// NextAuth
|
|
85
|
+
NEXTAUTH_SECRET: "NextAuth secret for JWT signing",
|
|
86
|
+
NEXTAUTH_URL: "NextAuth callback URL (derived from APP_URL if not set)",
|
|
87
|
+
|
|
88
|
+
// Application
|
|
89
|
+
APP_NAME: "Application name - used in metadata, emails, and page titles",
|
|
90
|
+
APP_DESCRIPTION:
|
|
91
|
+
"Application description - used for SEO metadata and social previews",
|
|
92
|
+
APP_URL: "Canonical application URL - derives NEXTAUTH_URL and CORS origins",
|
|
93
|
+
NEXT_PUBLIC_UMAMI_URL:
|
|
94
|
+
"Public Umami base URL for script.js and replay uploads",
|
|
95
|
+
NEXT_PUBLIC_UMAMI_WEBSITE_ID: "Public Umami website UUID",
|
|
96
|
+
NEXT_PUBLIC_UMAMI_REPLAY_ENABLED:
|
|
97
|
+
"Enable local rrweb session replay uploads to Umami",
|
|
98
|
+
ALLOW_INDEXING:
|
|
99
|
+
'Set to "true" to allow search engine indexing - only set in the production deployment',
|
|
100
|
+
NODE_ENV: "Environment (development, test, staging, production)",
|
|
101
|
+
PORT: "Web server port",
|
|
102
|
+
AUTH_ALLOW_SIGNUP:
|
|
103
|
+
'Set to "false" to disable public self-service signup routes and UI',
|
|
104
|
+
|
|
105
|
+
// Worker
|
|
106
|
+
WORKER_CONCURRENCY: "Number of concurrent jobs per queue (default: 5)",
|
|
107
|
+
|
|
108
|
+
// Storage
|
|
109
|
+
STORAGE_PROVIDER: 'Storage provider - "local" (default) or "s3"',
|
|
110
|
+
STORAGE_LOCAL_DIR: "Local storage directory",
|
|
111
|
+
S3_BUCKET: "S3 bucket name",
|
|
112
|
+
S3_REGION: "S3 region",
|
|
113
|
+
S3_ENDPOINT:
|
|
114
|
+
"S3-compatible endpoint URL (required for non-AWS providers: R2, MinIO, etc.)",
|
|
115
|
+
S3_ACCESS_KEY_ID: "S3 access key",
|
|
116
|
+
S3_SECRET_ACCESS_KEY: "S3 secret key",
|
|
117
|
+
S3_PUBLIC_URL: "S3 public URL prefix",
|
|
118
|
+
ASSET_CDN_URL:
|
|
119
|
+
"Public CDN base URL for asset delivery - provider-agnostic (CloudFront, Cloudflare, Bunny, etc.). Falls back to /api/files when unset.",
|
|
120
|
+
|
|
121
|
+
// Database instrumentation
|
|
122
|
+
DB_INSTRUMENTATION:
|
|
123
|
+
'Database query instrumentation — set to "false", "0", or "off" to disable Prisma $extends middleware with parameter masking (default: enabled)',
|
|
124
|
+
DB_SLOW_QUERY_THRESHOLD:
|
|
125
|
+
"Slow query threshold in milliseconds — queries exceeding this duration are logged as warnings (default: 500)",
|
|
126
|
+
|
|
127
|
+
// Admin
|
|
128
|
+
ADMIN_API_TOKEN:
|
|
129
|
+
"Bearer token protecting admin-only API routes (e.g. /api/admin/db-health). Strongly recommended in production.",
|
|
130
|
+
|
|
131
|
+
// Admin seed (used by `pnpm db:seed` — not required at web/worker runtime)
|
|
132
|
+
ADMIN_EMAIL:
|
|
133
|
+
"Admin user email address for initial database seed (default: admin@example.com)",
|
|
134
|
+
ADMIN_PASSWORD:
|
|
135
|
+
"Admin user password for initial database seed - required by seed script, min 12 characters",
|
|
136
|
+
ADMIN_NAME:
|
|
137
|
+
"Admin user display name for initial database seed (default: Admin)",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const ENV_KEYS = Object.keys(ENV_DESCRIPTIONS);
|
|
141
|
+
|
|
142
|
+
const optionalString = z.string().optional();
|
|
143
|
+
|
|
144
|
+
const absoluteHttpUrl = (message) =>
|
|
145
|
+
z
|
|
146
|
+
.string()
|
|
147
|
+
.refine((value) => isAbsoluteHttpUrl(value.trim()), { message })
|
|
148
|
+
.optional();
|
|
149
|
+
|
|
150
|
+
const booleanLike = (message) =>
|
|
151
|
+
z
|
|
152
|
+
.string()
|
|
153
|
+
.refine((value) => isBooleanLike(value), { message })
|
|
154
|
+
.optional();
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds a Zod schema for environment validation.
|
|
158
|
+
*
|
|
159
|
+
* @param {"web" | "worker"} service
|
|
160
|
+
* @returns {z.ZodObject}
|
|
161
|
+
*/
|
|
162
|
+
function createEnvSchema(service) {
|
|
163
|
+
const isTest = process.env.NODE_ENV === "test";
|
|
164
|
+
const nextAuthRequired = service !== "worker";
|
|
165
|
+
|
|
166
|
+
const nextAuthSecret = nextAuthRequired
|
|
167
|
+
? z
|
|
168
|
+
.string({
|
|
169
|
+
error: `Missing required environment variable: NEXTAUTH_SECRET (${ENV_DESCRIPTIONS.NEXTAUTH_SECRET})`,
|
|
170
|
+
})
|
|
171
|
+
.min(
|
|
172
|
+
32,
|
|
173
|
+
`NEXTAUTH_SECRET must be at least 32 characters (${ENV_DESCRIPTIONS.NEXTAUTH_SECRET})`,
|
|
174
|
+
)
|
|
175
|
+
: z
|
|
176
|
+
.string()
|
|
177
|
+
.min(
|
|
178
|
+
32,
|
|
179
|
+
`NEXTAUTH_SECRET must be at least 32 characters (${ENV_DESCRIPTIONS.NEXTAUTH_SECRET})`,
|
|
180
|
+
)
|
|
181
|
+
.optional();
|
|
182
|
+
|
|
183
|
+
return z
|
|
184
|
+
.object({
|
|
185
|
+
// Database
|
|
186
|
+
DATABASE_URL: optionalString,
|
|
187
|
+
POSTGRES_HOST: optionalString,
|
|
188
|
+
POSTGRES_PORT: optionalString,
|
|
189
|
+
POSTGRES_USER: optionalString,
|
|
190
|
+
POSTGRES_PASSWORD: optionalString,
|
|
191
|
+
POSTGRES_DB: optionalString,
|
|
192
|
+
|
|
193
|
+
// Redis
|
|
194
|
+
REDIS_URL: optionalString,
|
|
195
|
+
REDIS_HOST: optionalString,
|
|
196
|
+
REDIS_PORT: optionalString,
|
|
197
|
+
|
|
198
|
+
// Mail (local SMTP - Mailpit in dev)
|
|
199
|
+
MAIL_HOST: optionalString,
|
|
200
|
+
MAIL_SMTP_PORT: optionalString,
|
|
201
|
+
MAIL_UI_PORT: optionalString,
|
|
202
|
+
|
|
203
|
+
// Production SMTP
|
|
204
|
+
SMTP_HOST: optionalString,
|
|
205
|
+
SMTP_PORT: optionalString,
|
|
206
|
+
SMTP_SECURE: optionalString,
|
|
207
|
+
SMTP_USER: optionalString,
|
|
208
|
+
SMTP_PASSWORD: optionalString,
|
|
209
|
+
|
|
210
|
+
// Email provider
|
|
211
|
+
EMAIL_PROVIDER: optionalString,
|
|
212
|
+
EMAIL_FROM: optionalString,
|
|
213
|
+
RESEND_API_KEY: optionalString,
|
|
214
|
+
ZEPTOMAIL_TOKEN: optionalString,
|
|
215
|
+
ZEPTOMAIL_URL: optionalString,
|
|
216
|
+
ZEPTOMAIL_BOUNCE_EMAIL: optionalString,
|
|
217
|
+
|
|
218
|
+
// NextAuth
|
|
219
|
+
NEXTAUTH_SECRET: nextAuthSecret,
|
|
220
|
+
NEXTAUTH_URL: absoluteHttpUrl(
|
|
221
|
+
"NEXTAUTH_URL must be an absolute http(s) URL",
|
|
222
|
+
),
|
|
223
|
+
|
|
224
|
+
// Application
|
|
225
|
+
APP_NAME: optionalString,
|
|
226
|
+
APP_DESCRIPTION: optionalString,
|
|
227
|
+
APP_URL: absoluteHttpUrl("APP_URL must be an absolute http(s) URL"),
|
|
228
|
+
NEXT_PUBLIC_UMAMI_URL: absoluteHttpUrl(
|
|
229
|
+
"NEXT_PUBLIC_UMAMI_URL must be an absolute http(s) URL",
|
|
230
|
+
),
|
|
231
|
+
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z
|
|
232
|
+
.string()
|
|
233
|
+
.refine((value) => isUuid(value), {
|
|
234
|
+
message: "NEXT_PUBLIC_UMAMI_WEBSITE_ID must be a UUID",
|
|
235
|
+
})
|
|
236
|
+
.optional(),
|
|
237
|
+
NEXT_PUBLIC_UMAMI_REPLAY_ENABLED: booleanLike(
|
|
238
|
+
"NEXT_PUBLIC_UMAMI_REPLAY_ENABLED must be a boolean-like value (true/false/1/0/yes/no/on/off)",
|
|
239
|
+
),
|
|
240
|
+
ALLOW_INDEXING: optionalString,
|
|
241
|
+
NODE_ENV: optionalString,
|
|
242
|
+
PORT: optionalString,
|
|
243
|
+
AUTH_ALLOW_SIGNUP: booleanLike(
|
|
244
|
+
"AUTH_ALLOW_SIGNUP must be a boolean-like value (true/false/1/0/yes/no/on/off)",
|
|
245
|
+
),
|
|
246
|
+
|
|
247
|
+
// Worker
|
|
248
|
+
WORKER_CONCURRENCY: optionalString,
|
|
249
|
+
|
|
250
|
+
// Storage
|
|
251
|
+
STORAGE_PROVIDER: optionalString,
|
|
252
|
+
STORAGE_LOCAL_DIR: optionalString,
|
|
253
|
+
S3_BUCKET: optionalString,
|
|
254
|
+
S3_REGION: optionalString,
|
|
255
|
+
S3_ENDPOINT: optionalString,
|
|
256
|
+
S3_ACCESS_KEY_ID: optionalString,
|
|
257
|
+
S3_SECRET_ACCESS_KEY: optionalString,
|
|
258
|
+
S3_PUBLIC_URL: optionalString,
|
|
259
|
+
ASSET_CDN_URL: optionalString,
|
|
260
|
+
|
|
261
|
+
// Database instrumentation
|
|
262
|
+
DB_INSTRUMENTATION: optionalString,
|
|
263
|
+
DB_SLOW_QUERY_THRESHOLD: optionalString,
|
|
264
|
+
|
|
265
|
+
// Admin
|
|
266
|
+
ADMIN_API_TOKEN: optionalString,
|
|
267
|
+
ADMIN_EMAIL: optionalString,
|
|
268
|
+
ADMIN_PASSWORD: optionalString,
|
|
269
|
+
ADMIN_NAME: optionalString,
|
|
270
|
+
})
|
|
271
|
+
.superRefine((data, ctx) => {
|
|
272
|
+
// Placeholder value security check
|
|
273
|
+
const placeholderPattern = /^CHANGE_ME_/i;
|
|
274
|
+
const criticalKeys = [
|
|
275
|
+
"NEXTAUTH_SECRET",
|
|
276
|
+
"AUTH_SECRET",
|
|
277
|
+
"POSTGRES_PASSWORD",
|
|
278
|
+
"RESEND_API_KEY",
|
|
279
|
+
"ZEPTOMAIL_TOKEN",
|
|
280
|
+
"S3_SECRET_ACCESS_KEY",
|
|
281
|
+
"SMTP_PASSWORD",
|
|
282
|
+
];
|
|
283
|
+
for (const key of criticalKeys) {
|
|
284
|
+
const value = process.env[key];
|
|
285
|
+
if (value && placeholderPattern.test(value)) {
|
|
286
|
+
ctx.addIssue({
|
|
287
|
+
code: "custom",
|
|
288
|
+
message: `${key} contains a placeholder value - replace with a real secret (${ENV_DESCRIPTIONS[key] || ""})`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Database: either DATABASE_URL or POSTGRES_USER must be set (skip in test)
|
|
294
|
+
if (!isTest) {
|
|
295
|
+
const hasDbUrl = !!process.env.DATABASE_URL;
|
|
296
|
+
const hasPostgresUser = !!process.env.POSTGRES_USER;
|
|
297
|
+
if (!hasDbUrl && !hasPostgresUser) {
|
|
298
|
+
ctx.addIssue({
|
|
299
|
+
code: "custom",
|
|
300
|
+
message:
|
|
301
|
+
"Database not configured: set DATABASE_URL or POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Conditional: S3 storage requires bucket + credentials
|
|
307
|
+
if (process.env.STORAGE_PROVIDER === "s3") {
|
|
308
|
+
for (const key of [
|
|
309
|
+
"S3_BUCKET",
|
|
310
|
+
"S3_ACCESS_KEY_ID",
|
|
311
|
+
"S3_SECRET_ACCESS_KEY",
|
|
312
|
+
]) {
|
|
313
|
+
if (!process.env[key]) {
|
|
314
|
+
ctx.addIssue({
|
|
315
|
+
code: "custom",
|
|
316
|
+
message: `Missing ${key} - required when STORAGE_PROVIDER=s3 (${ENV_DESCRIPTIONS[key]})`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Conditional: Resend provider requires API key
|
|
323
|
+
if (
|
|
324
|
+
process.env.EMAIL_PROVIDER === "resend" &&
|
|
325
|
+
!process.env.RESEND_API_KEY
|
|
326
|
+
) {
|
|
327
|
+
ctx.addIssue({
|
|
328
|
+
code: "custom",
|
|
329
|
+
message:
|
|
330
|
+
"Missing RESEND_API_KEY - required when EMAIL_PROVIDER=resend",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Conditional: Zeptomail provider requires token and URL
|
|
335
|
+
if (process.env.EMAIL_PROVIDER === "zeptomail") {
|
|
336
|
+
if (!process.env.ZEPTOMAIL_TOKEN) {
|
|
337
|
+
ctx.addIssue({
|
|
338
|
+
code: "custom",
|
|
339
|
+
message:
|
|
340
|
+
"Missing ZEPTOMAIL_TOKEN - required when EMAIL_PROVIDER=zeptomail",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (!process.env.ZEPTOMAIL_URL) {
|
|
344
|
+
ctx.addIssue({
|
|
345
|
+
code: "custom",
|
|
346
|
+
message:
|
|
347
|
+
"Missing ZEPTOMAIL_URL - required when EMAIL_PROVIDER=zeptomail",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Umami replay requires full analytics contract
|
|
353
|
+
const umamiUrl = data.NEXT_PUBLIC_UMAMI_URL?.trim();
|
|
354
|
+
const umamiWebsiteId = data.NEXT_PUBLIC_UMAMI_WEBSITE_ID?.trim();
|
|
355
|
+
const umamiReplayEnabled = data.NEXT_PUBLIC_UMAMI_REPLAY_ENABLED?.trim();
|
|
356
|
+
const hasUmamiUrl = Boolean(umamiUrl);
|
|
357
|
+
const hasUmamiWebsiteId = Boolean(umamiWebsiteId);
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
umamiReplayEnabled &&
|
|
361
|
+
isTruthyLike(umamiReplayEnabled) &&
|
|
362
|
+
(!hasUmamiUrl || !hasUmamiWebsiteId)
|
|
363
|
+
) {
|
|
364
|
+
ctx.addIssue({
|
|
365
|
+
code: "custom",
|
|
366
|
+
message:
|
|
367
|
+
"NEXT_PUBLIC_UMAMI_REPLAY_ENABLED requires NEXT_PUBLIC_UMAMI_URL and NEXT_PUBLIC_UMAMI_WEBSITE_ID.",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Collects non-fatal environment warnings.
|
|
375
|
+
*
|
|
376
|
+
* @returns {string[]}
|
|
377
|
+
*/
|
|
378
|
+
function collectWarnings() {
|
|
379
|
+
const warnings = [];
|
|
380
|
+
const isTest = process.env.NODE_ENV === "test";
|
|
381
|
+
const currentEnv = (process.env.NODE_ENV || "").toLowerCase();
|
|
382
|
+
const isProductionLike =
|
|
383
|
+
currentEnv === "production" || currentEnv === "staging";
|
|
384
|
+
|
|
385
|
+
// Redis: warn if not configured (defaults to localhost in dev, will fail in prod)
|
|
386
|
+
if (
|
|
387
|
+
!process.env.REDIS_URL &&
|
|
388
|
+
!process.env.REDIS_HOST &&
|
|
389
|
+
(currentEnv === "production" || currentEnv === "staging")
|
|
390
|
+
) {
|
|
391
|
+
warnings.push(
|
|
392
|
+
"Redis not configured: set REDIS_URL or REDIS_HOST (defaults to localhost)",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// SEO metadata: APP_DESCRIPTION should be explicitly set before production
|
|
397
|
+
if (!isTest && isProductionLike) {
|
|
398
|
+
const appDescription = process.env.APP_DESCRIPTION;
|
|
399
|
+
if (!appDescription) {
|
|
400
|
+
warnings.push(
|
|
401
|
+
"APP_DESCRIPTION not set: metadata description will fall back to a generic value. Set APP_DESCRIPTION before production.",
|
|
402
|
+
);
|
|
403
|
+
} else if (/^CHANGE_ME_|^TODO_/i.test(appDescription)) {
|
|
404
|
+
warnings.push(
|
|
405
|
+
"APP_DESCRIPTION appears to be a placeholder value. Update it before production.",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Canonical origin: without it, Auth.js derives localhost and post-sign-in
|
|
411
|
+
// redirects can point at a different origin than the one the cookie was
|
|
412
|
+
// issued for (symptom: you land back on the homepage signed out).
|
|
413
|
+
if (!isTest && isProductionLike && !process.env.APP_URL) {
|
|
414
|
+
warnings.push(
|
|
415
|
+
"APP_URL not set: Auth.js and CORS origins will fall back to http://localhost. Set APP_URL to your real https origin before production.",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Umami pair completeness
|
|
420
|
+
const umamiUrl = process.env.NEXT_PUBLIC_UMAMI_URL?.trim();
|
|
421
|
+
const umamiWebsiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID?.trim();
|
|
422
|
+
const hasUmamiUrl = Boolean(umamiUrl);
|
|
423
|
+
const hasUmamiWebsiteId = Boolean(umamiWebsiteId);
|
|
424
|
+
if (hasUmamiUrl !== hasUmamiWebsiteId) {
|
|
425
|
+
warnings.push(
|
|
426
|
+
"Umami analytics is incomplete: set both NEXT_PUBLIC_UMAMI_URL and NEXT_PUBLIC_UMAMI_WEBSITE_ID to enable tracking.",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return warnings;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Builds the env input object for Zod parsing.
|
|
435
|
+
* Only includes defined, non-empty values (matching prior validated output).
|
|
436
|
+
*
|
|
437
|
+
* @returns {Record<string, string>}
|
|
438
|
+
*/
|
|
439
|
+
function buildEnvInput() {
|
|
440
|
+
/** @type {Record<string, string>} */
|
|
441
|
+
const input = {};
|
|
442
|
+
|
|
443
|
+
for (const key of ENV_KEYS) {
|
|
444
|
+
const value =
|
|
445
|
+
key === "NEXTAUTH_SECRET"
|
|
446
|
+
? getResolvedNextAuthSecret()
|
|
447
|
+
: process.env[key];
|
|
448
|
+
|
|
449
|
+
if (value) {
|
|
450
|
+
input[key] = value;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return input;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Validates environment variables against schema.
|
|
459
|
+
*
|
|
460
|
+
* @param {"web" | "worker"} [service="web"] - The service being validated.
|
|
461
|
+
* Worker skips web-only checks (e.g. NEXTAUTH_SECRET).
|
|
462
|
+
* @throws {ValidationError} If required environment variables are missing
|
|
463
|
+
* @returns {{ validated: Object, warnings: string[] }}
|
|
464
|
+
*/
|
|
465
|
+
export function validateEnv(service = "web") {
|
|
466
|
+
// During Next.js build, runtime secrets are not (and should not be) available.
|
|
467
|
+
// Skip strict validation - the server process will re-validate on first request.
|
|
468
|
+
if (process.env.NEXT_PHASE === "phase-production-build") {
|
|
469
|
+
return { validated: {}, warnings: [] };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const warnings = collectWarnings();
|
|
473
|
+
|
|
474
|
+
// Log warnings (non-fatal)
|
|
475
|
+
for (const warning of warnings) {
|
|
476
|
+
log.warn(warning);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const schema = createEnvSchema(service);
|
|
480
|
+
const input = buildEnvInput();
|
|
481
|
+
const result = schema.safeParse(input);
|
|
482
|
+
|
|
483
|
+
if (!result.success) {
|
|
484
|
+
const errors = result.error.issues.map((issue) => issue.message);
|
|
485
|
+
const errorMessage = `Environment Validation Failed:\n${errors.join("\n")}`;
|
|
486
|
+
throw new ValidationError(errorMessage);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** @type {Record<string, string>} */
|
|
490
|
+
const validated = {};
|
|
491
|
+
for (const [key, value] of Object.entries(result.data)) {
|
|
492
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
493
|
+
validated[key] = value;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Ensure NEXTAUTH_URL is derived from APP_URL when not explicitly set
|
|
498
|
+
if (service === "web") {
|
|
499
|
+
if (!process.env.NEXTAUTH_SECRET && process.env.AUTH_SECRET) {
|
|
500
|
+
process.env.NEXTAUTH_SECRET = process.env.AUTH_SECRET;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
syncNextAuthUrl();
|
|
504
|
+
|
|
505
|
+
// Include the (possibly derived) NEXTAUTH_URL in the validated object
|
|
506
|
+
if (process.env.NEXTAUTH_URL && !validated.NEXTAUTH_URL) {
|
|
507
|
+
validated.NEXTAUTH_URL = process.env.NEXTAUTH_URL;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (process.env.NEXTAUTH_SECRET && !validated.NEXTAUTH_SECRET) {
|
|
511
|
+
validated.NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return { validated, warnings };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Loads and validates environment variables.
|
|
520
|
+
* Call this function at application startup.
|
|
521
|
+
*
|
|
522
|
+
* @param {"web" | "worker"} [service="web"]
|
|
523
|
+
*/
|
|
524
|
+
export function loadEnv(service = "web") {
|
|
525
|
+
try {
|
|
526
|
+
const { validated } = validateEnv(service);
|
|
527
|
+
return validated;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
log.error(error.message);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @yourscope/jobs
|
|
2
|
+
|
|
3
|
+
Scaffolded background job definitions for your Quark project.
|
|
4
|
+
|
|
5
|
+
This package is **yours** after scaffolding - edit the queues, job names, and payload conventions to match your product. There is no automatic sync back to Quark.
|
|
6
|
+
|
|
7
|
+
## Use this package when
|
|
8
|
+
|
|
9
|
+
- work should happen outside the web request cycle
|
|
10
|
+
- you need retries, backoff, or queue-based processing
|
|
11
|
+
- you want a stable contract between the web app and `apps/worker`
|
|
12
|
+
|
|
13
|
+
## What it owns
|
|
14
|
+
|
|
15
|
+
- `src/definitions.js` - queue names and job names
|
|
16
|
+
- `src/index.js` - package exports
|
|
17
|
+
|
|
18
|
+
The actual job execution happens in the paired worker app:
|
|
19
|
+
|
|
20
|
+
- `apps/worker/src/handlers/`
|
|
21
|
+
- `apps/worker/src/index.js`
|
|
22
|
+
|
|
23
|
+
## First files to edit in a scaffolded project
|
|
24
|
+
|
|
25
|
+
1. `packages/jobs/src/definitions.js` - add queue names and job names
|
|
26
|
+
2. `apps/worker/src/handlers/index.js` - register handlers
|
|
27
|
+
3. `apps/worker/src/handlers/*.js` - implement the actual job logic
|
|
28
|
+
|
|
29
|
+
## Default jobs
|
|
30
|
+
|
|
31
|
+
The starter template ships with examples for:
|
|
32
|
+
|
|
33
|
+
- welcome email delivery
|
|
34
|
+
- reset-password email delivery
|
|
35
|
+
- orphaned file cleanup
|
|
36
|
+
|
|
37
|
+
Treat them as examples, not a required domain model.
|
|
38
|
+
|
|
39
|
+
## Add a new job
|
|
40
|
+
|
|
41
|
+
1. Add a queue or job name in `src/definitions.js`
|
|
42
|
+
2. Create a handler in `apps/worker/src/handlers/`
|
|
43
|
+
3. Register that handler in `apps/worker/src/handlers/index.js`
|
|
44
|
+
4. Dispatch the job from the web app using `createQueue()` and `addJob()` from `@techstream/quark-core`
|
|
45
|
+
|
|
46
|
+
## Relationship to Quark Core
|
|
47
|
+
|
|
48
|
+
`@techstream/quark-core` provides the queue infrastructure.
|
|
49
|
+
|
|
50
|
+
`@yourscope/jobs` defines **your** queue names and job names.
|
|
51
|
+
|
|
52
|
+
That split lets you update infrastructure separately from business-specific job contracts.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const JOB_QUEUES = {
|
|
2
|
+
EMAIL: "email-queue",
|
|
3
|
+
FILES: "files-queue",
|
|
4
|
+
PUSH: "push-queue",
|
|
5
|
+
DEFAULT: "default-queue",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const JOB_NAMES = {
|
|
9
|
+
SEND_WELCOME_EMAIL: "send-welcome-email",
|
|
10
|
+
SEND_RESET_PASSWORD_EMAIL: "send-reset-password-email",
|
|
11
|
+
CLEANUP_ORPHANED_FILES: "cleanup-orphaned-files",
|
|
12
|
+
SEND_PUSH_NOTIFICATION: "send-push-notification",
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./definitions.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Mobile App Environment Configuration
|
|
2
|
+
# Copy this file to .env and fill in the values.
|
|
3
|
+
# NEVER commit the .env file to version control - it contains secrets!
|
|
4
|
+
# $ cp .env.example .env
|
|
5
|
+
|
|
6
|
+
# --- API Configuration ---
|
|
7
|
+
# The URL of your Quark web app backend (no trailing slash)
|
|
8
|
+
EXPO_PUBLIC_API_URL=http://localhost:3000
|
|
9
|
+
|
|
10
|
+
# --- Google OAuth (Optional) ---
|
|
11
|
+
# Get credentials at: https://console.cloud.google.com/apis/credentials
|
|
12
|
+
# EXPO_PUBLIC_GOOGLE_CLIENT_ID=your_google_client_id
|
|
13
|
+
|
|
14
|
+
# --- Apple Sign In (Optional) ---
|
|
15
|
+
# Apple Sign In is configured via EAS and Apple Developer Console.
|
|
16
|
+
# No environment variables needed - it uses native module configuration.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# OTA vs Store Build Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist to determine whether a change can be deployed via OTA update or requires a new store build.
|
|
4
|
+
|
|
5
|
+
## OTA Update (expo-updates)
|
|
6
|
+
|
|
7
|
+
Changes that CAN be delivered via OTA:
|
|
8
|
+
- JavaScript/TypeScript code changes
|
|
9
|
+
- Asset updates (images, fonts, colors)
|
|
10
|
+
- Route structure changes (file-based routing)
|
|
11
|
+
- API endpoint URL changes
|
|
12
|
+
- UI component updates
|
|
13
|
+
- Bug fixes in existing features
|
|
14
|
+
|
|
15
|
+
## Store Build Required
|
|
16
|
+
|
|
17
|
+
Changes that CANNOT be delivered via OTA:
|
|
18
|
+
- Native module additions or removals
|
|
19
|
+
- `app.json` / `app.config.js` changes to native config
|
|
20
|
+
- New expo plugin additions
|
|
21
|
+
- Changes to `expo-notifications` plugin config
|
|
22
|
+
- iOS entitlement changes
|
|
23
|
+
- Android permission changes
|
|
24
|
+
- React Native version upgrades
|
|
25
|
+
- Expo SDK major version upgrades
|
|
26
|
+
|
|
27
|
+
## How to Check
|
|
28
|
+
|
|
29
|
+
1. Run `git diff` on `package.json`
|
|
30
|
+
2. If native dependencies changed → store build needed
|
|
31
|
+
3. If only JS/TS files changed → OTA is safe
|
|
32
|
+
4. Test OTA on staging channel before production
|
|
33
|
+
|
|
34
|
+
## CI Integration
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Check for native dependency changes
|
|
38
|
+
git diff origin/main...HEAD -- package.json | grep -E "^\+.*(expo-|react-native)"
|
|
39
|
+
# If output exists, block OTA and require store build
|
|
40
|
+
```
|