Repository navigation
Expand file tree
/
Copy pathwrangler.jsonc
More file actions
227 lines (219 loc) · 10.6 KB
/
Copy pathwrangler.jsonc
File metadata and controls
227 lines (219 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "inkloom",
"main": "./workers/app.ts",
"compatibility_date": "2026-09-01",
// `nodejs_compat` is what lets the `pg` driver run in workerd, so the same
// database code path serves local development, tests and production.
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./build/client",
// The Worker runs first and forwards everything it does not serve as a
// static file, so /api/* and SSR both reach the handler.
"run_worker_first": ["/api/*"],
"not_found_handling": "none",
},
"observability": { "enabled": true, "head_sampling_rate": 1 },
/*
* Run the Worker near the DATABASE, not near the visitor.
*
* A signed-in page makes several sequential queries and each one pays the full
* round trip between the edge and Neon. Measured on staging with the Worker at
* Mumbai and the database in Singapore (~70ms apart): authenticated pages
* averaged 837ms, marketing pages 49ms. That gap is round trips, not compute.
* Running the Worker beside the database turns N round trips into one.
*
* A PINNED REGION, not `mode: "smart"`, and that is a measured decision rather
* than a preference. Smart mode was deployed first and did nothing: the
* response header stayed `cf-placement: local-BOM` and authenticated latency
* did not move. Smart Placement needs sustained traffic before it will make a
* decision, and this application serves tens of requests an hour — it would
* have sat un-relocated indefinitely while appearing to be configured. The
* pinned region reported `cf-placement: remote-SIN` on the very next deploy.
*
* The cost is real and was measured too: public pages went from ~92ms to
* ~190ms at p50, because they now pay a hop to Singapore for work that barely
* touches the database. That is the trade — it is worth it only while the
* signed-in experience dominates, and it should be re-measured if that stops
* being true.
*
* THIS MUST TRACK THE NEON REGION. If the database moves, this becomes
* silently wrong: still valid, still deployed, just slow again with nothing to
* say why. `cf-placement` in any response header is how to check it.
*/
"placement": { "region": "aws:ap-southeast-1" },
/*
* Retention sweep.
*
* 03:20 UTC daily — off the hour, because every cron on the platform fires at
* :00 and a sweep has no reason to queue behind them. Nothing else enforces
* the retention periods published at /privacy, so if this trigger is missing
* the policy is simply not kept.
*
* Cron triggers are per-environment in Cloudflare and are NOT inherited from
* this top-level block, which is why staging and production each declare
* their own below.
*/
"triggers": { "crons": ["20 3 * * *"] },
// Hyperdrive pools Postgres connections at Cloudflare's edge.
//
// It is used in EVERY environment, not just production, for one specific
// reason: a Worker isolate cannot safely reuse a raw socket across request
// contexts, so a cached `pg` pool eventually serves a dead connection and
// queries start timing out. Hyperdrive owns the pooling, so the Worker gets a
// usable connection every time.
//
// Locally, `localConnectionString` points Wrangler straight at the Docker
// Postgres, so dev and production run the identical code path. The staging
// and production entries below carry real Hyperdrive ids pointing at separate
// Neon branches.
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "inkloom-local-dev",
"localConnectionString": "postgresql://inkloom:inkloom_local_dev@127.0.0.1:5433/inkloom",
},
],
// ---------------------------------------------------------------------
// NON-SECRET values only.
//
// Every secret is set with `wrangler secret put NAME --env <env>` and lives
// in Cloudflare Secrets, never in this file:
// BETTER_AUTH_SECRET, ACCESS_CODE_PEPPER, IP_HASH_PEPPER,
// RESEND_API_KEY, TURNSTILE_SECRET_KEY, GOOGLE_CLIENT_SECRET
// The Hyperdrive binding carries the database credentials, so DATABASE_URL
// is not set here either.
// ---------------------------------------------------------------------
"vars": {
"INKLOOM_ENV": "development",
"APP_URL": "http://localhost:5173",
"EMAIL_TRANSPORT": "mailpit",
"MAILPIT_HOST": "127.0.0.1",
"EMAIL_FROM": "Inkloom <no-reply@mail.inkloom.art>",
"EMAIL_REPLY_TO": "support@inkloom.art",
"SUPPORT_EMAIL": "support@inkloom.art",
// ADMIN_PATH and OWNER_EMAIL are deployment secrets set with
// `wrangler secret put`, NOT here. ADMIN_PATH must ALSO be present in the
// build environment — the route table bakes it in so the console's links
// are correct. See docs/DEPLOYMENT.md.
"TURNSTILE_SITE_KEY": "1x00000000000000000000AA",
"TURNSTILE_ENABLED": "true",
"RATE_LIMIT_ENABLED": "true",
"ANALYTICS_ENABLED": "true",
"LOG_LEVEL": "debug",
"INKLOOM_RELEASE": "dev",
},
"env": {
"staging": {
"name": "inkloom-staging",
"vars": {
"INKLOOM_ENV": "staging",
"APP_URL": "https://staging.inkloom.art",
"EMAIL_TRANSPORT": "resend",
"EMAIL_FROM": "Inkloom (staging) <no-reply@mail-staging.inkloom.art>",
"EMAIL_REPLY_TO": "support@inkloom.art",
"SUPPORT_EMAIL": "support@inkloom.art",
"TURNSTILE_ENABLED": "true",
// MUST be repeated here. `env.*.vars` REPLACES the top-level `vars`
// block rather than merging into it, so anything omitted falls back to
// its schema default — and the default for this one is "", which renders
// the widget with an empty sitekey. The form then submits no token while
// the server still demands one, breaking signup, login, password reset
// and contact all at once, with nothing in the logs but "captcha failed".
// loadConfig() now refuses to boot on an empty value for exactly this
// reason. This is the "inkloom-staging" widget, scoped to the
// staging.inkloom.art hostname only; production has its own.
"TURNSTILE_SITE_KEY": "0x4AAAAAAEy5d7NT7jtcSiLg",
"RATE_LIMIT_ENABLED": "true",
"ANALYTICS_ENABLED": "true",
"LOG_LEVEL": "info",
"SENTRY_ENVIRONMENT": "staging",
"EMAIL_DAILY_QUOTA": "0",
"EMAIL_MONTHLY_QUOTA": "50000",
"INKLOOM_RELEASE": "v1.0.0-staging",
},
// A SEPARATE Neon branch from production. Sharing one would put real
// user data in a test environment.
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "94340d8d7b7948ccb8ff1f246790fc60" }],
// Cron triggers are not inherited from the top-level block.
"triggers": { "crons": ["20 3 * * *"] },
"routes": [{ "pattern": "staging.inkloom.art", "custom_domain": true }],
/*
* NO workers.dev URL, and this is a security control rather than tidiness.
*
* A Cloudflare Access policy protects a HOSTNAME. With the workers.dev
* route enabled, inkloom-staging.<subdomain>.workers.dev reaches the same
* Worker with no Access in front of it at all — an environment everyone
* believes is private, published at a guessable address. The Worker also
* verifies the Access JWT itself (see ACCESS_TEAM_DOMAIN), so this is the
* second of two locks rather than the only one.
*/
"workers_dev": false,
},
"production": {
"name": "inkloom-production",
"vars": {
"INKLOOM_ENV": "production",
"APP_URL": "https://inkloom.art",
"EMAIL_TRANSPORT": "resend",
"EMAIL_FROM": "Inkloom <no-reply@mail.inkloom.art>",
"EMAIL_REPLY_TO": "support@inkloom.art",
"SUPPORT_EMAIL": "support@inkloom.art",
"TURNSTILE_ENABLED": "true",
// See the note in the staging block: this must be repeated per
// environment, and an empty value silently breaks every form.
// This is the "inkloom-production" widget, scoped to inkloom.art only;
// staging has its own, so a problem with one cannot reach the other.
"TURNSTILE_SITE_KEY": "0x4AAAAAAE8XLbusGd7fnZuj",
"RATE_LIMIT_ENABLED": "true",
"ANALYTICS_ENABLED": "true",
"LOG_LEVEL": "info",
"SENTRY_ENVIRONMENT": "production",
/*
* Resend Pro: 50,000 a month, and NO daily ceiling.
*
* The nightly health check warns at 80% of the monthly figure and
* raises a critical at 100%, because every signup needs a verification
* email — so the mail quota IS the signup capacity, and running out is
* silent: the account is created and the message never leaves.
*
* The daily value is 0 because a paid plan has no daily limit. It was
* 100 — the FREE plan's ceiling — which survived the upgrade and then
* declared a critical incident at 131 messages on an account with
* 49,000 left in the month. A quota written down here is a claim about
* somebody else's system, so it has to be corrected here when their
* side changes; `email_provider_quota` in the logs carries the
* provider's own figure for when this drifts again.
*/
"EMAIL_DAILY_QUOTA": "0",
"EMAIL_MONTHLY_QUOTA": "50000",
"BACKUPS_EXPECTED": "true",
"INKLOOM_RELEASE": "v1.0.0",
},
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "4fc8b31d98b642539478843fb51e677c" }],
// Cron triggers are not inherited from the top-level block.
"triggers": { "crons": ["20 3 * * *"] },
/*
* Both hostnames reach the Worker; the Worker 308s www to the apex.
* Without the www entry its proxied CNAME has no origin behind it and
* Cloudflare answers 522, which looks like an outage rather than a
* slightly wrong address.
*
* A ROUTE for www, not a second custom domain, and that is not a
* preference. A custom domain insists on creating and owning the DNS
* record, so it refuses outright when one already exists —
* "Hostname 'www.inkloom.art' already has externally managed DNS records"
* — and the deploy fails after the Worker has already uploaded. A route
* attaches to the hostname the existing proxied record already resolves,
* which is exactly the situation here.
*/
"routes": [
{ "pattern": "inkloom.art", "custom_domain": true },
{ "pattern": "www.inkloom.art/*", "zone_name": "inkloom.art" },
],
// The product is served from its own domain; a second, un-branded address
// for the same Worker is only ever a way around a control set on the first.
"workers_dev": false,
},
},
}