chore(ops): move to a domain we control and proxy the origin through Cloudflare #313

Open
opened 2026-09-05 20:35:43 -05:00 by bermudalamb · 4 comments
Owner

Follow-on from #285, which established that Cloudflare's free tier cannot be applied to bermudalamb.synology.me at all — the free plan supports only full setup, which needs nameserver delegation for the zone, and the zone is Synology's. Partial (CNAME) setup is Business/Enterprise. The findings are in docs/ops/cloudflare-free-tier.md.

So origin hiding is blocked behind owning a domain. This issue is that migration.

This is not a DNS change. The hostname is embedded in outstanding emails, session cookies and signed links, and the application's proxy assumptions are wrong the moment a second proxy exists. Most of the work below is not Cloudflare configuration.

Scope

Register a domain we control, put its zone on Cloudflare, proxy the storefront through it, and keep the NAS as the origin behind DDNS.

The registrar decision

The registrar does nothing for origin hiding on its own — only the Cloudflare proxy does. Buying a domain and using the registrar's own DNS publishes the house IP exactly as today. The registrar's whole job is to own a zone we can delegate to Cloudflare, cheaply.

Registrar .com renewal WHOIS privacy 5-year Notes
Cloudflare Registrar ~$9.77 Free ~$49 At-cost. Must use Cloudflare nameservers
Porkbun ~$11 Free ~$55 NS delegatable anywhere
Namecheap ~$15 Free ~$75 NS delegatable anywhere
GoDaddy $22.99 (yr 1 $0.99) +$9.99/yr ~$122 Teaser pricing; privacy is a paid add-on

Proposed: Cloudflare Registrar. Cheapest, and its one restriction — no external nameservers — is the thing we actually want here. The trade-off is lock-in: leaving Cloudflare later means transferring the domain out, not repointing NS. Porkbun is the hedge if that flexibility is wanted, at about a dollar a year more.

Prices are from third-party comparison sites as of 2026-09-05, not each registrar's own pricing page. Confirm at purchase.

Two things that must ship in the same change as the DNS cutover

1. trust proxy is wrong the moment Cloudflare is in front

app.ts:34 sets app.set('trust proxy', 1). Today the chain is client → NPM → app and req.ip is the real client. Cloudflare appends to X-Forwarded-For rather than replacing it, so the chain becomes client → Cloudflare → NPM → app and req.ip resolves to a Cloudflare edge address.

Four limiters key on req.ip and would silently stop distinguishing callers while continuing to look healthy — one abusive client would spend everybody's allowance:

  • passwordResetRequestLimiter (via keyByCallerAndEmail)
  • clientErrorLimiter (no keyGenerator, so it defaults to IP)
  • intakeViewLimiter
  • intakeSubmitLimiter

verificationResendLimiter is unaffected; it keys on customer id.

Fix is trust proxy: 2, or read CF-Connecting-IP and validate the peer against Cloudflare's published ranges. The comment at rateLimit.ts:62-64 asserts the current behaviour as fact and has to change in the same commit, or it becomes a lie that the next person will believe.

This is latent for any second proxy, not only Cloudflare.

2. The proxied flag is a silent-failure trap

The home IP is dynamic, so a DDNS updater must keep the Cloudflare A record current via their API. That API call carries a proxied boolean.

If the updater ever writes proxied: false, the origin IP goes public and nothing breaks. The site keeps serving, so there is no signal — the protection is simply gone. Same shape as the trust proxy problem: correct-looking behaviour, absent protection.

Requirements:

  • The updater must assert proxied: true on every write, never inherit it.
  • Something should periodically verify the record is still proxied and complain if it is not. An unmonitored boolean protecting the thing this whole migration exists for is not good enough.

One upside: with a proxied record, visitors always terminate at Cloudflare, so an origin IP change needs no client-side DNS propagation. Strictly better than plain DDNS today.

The migration surface

Verified in the code rather than guessed:

  • docker-compose.prod.yml:191 hardcodes PUBLIC_URL=https://redefined-designs.bermudalamb.synology.me.
  • Eight backend files build customer-facing URLs from PUBLIC_URL: envValidation.ts, favoriteAlerts.ts, intake/actionLinks.ts, intake/notifyDraft.ts, routes/adminUploadLinks.ts, routes/customers.ts, server.ts, utils.ts.
  • UPLOADS_BASE_URL is a separate origin and needs deciding alongside.
  • docker-compose.qa.yml carries the QA hostname in its documented env block.
  • NPM's authentik forward-auth gates ^/(admin|api/admin). That config is not in this repo and has to move with the hostname. Per decision 5 in project-context.md, do not widen or narrow that regex in the process.
  • Gitea (gitea.bermudalamb.synology.me) resolves to the same address and the Actions runner needs it. Left unproxied it republishes the IP; proxied, Bot Fight Mode may challenge the runner.

The cutover window

Outstanding links and cookies already in customers' hands, with the values checked:

Thing Lifetime Consequence of the old host going away
Intake action links (ACTION_TTL_MS) 30 days, hardcoded Approve/reject links in admin mail stop working
Session cookie (SESSION_DAYS) 30 days Cookies are host-scoped — every customer is logged out at cutover regardless
Email verification (verify_token_hours) 24 h default, admin-configurable Pending verifications break
Password reset (password_reset_hours) 1 h default, admin-configurable In-flight resets break

So the old hostname has to keep answering for at least 30 days after cutover, or outstanding intake action links die. The logout is unavoidable and worth deciding whether to announce.

Upload-link expiry was not established — adminUploadLinks.ts bounds submissions with max_submissions and no expiry was found. Confirm before cutover.

Decisions this issue needs to make

  • Domain name, and registrar (proposed: Cloudflare Registrar).
  • Whether QA goes through Cloudflare too. If it stays direct it publishes the same address and defeats the exercise, so "leave QA alone" is not a neutral default.
  • Whether Gitea is proxied, accepting the Bot Fight Mode risk to the Actions runner, or left exposed.
  • Bot Fight Mode on or off. #285 found it cannot be skipped by WAF or Page Rules on any plan — it does not run on the Ruleset Engine — and Cloudflare documents that it may challenge API traffic. A challenged POST /webhooks/paypal is a capture notification we never receive. Safe answer is off.
  • Whether to pursue Authenticated Origin Pull, which is on the free plan and is the only piece that actually stops direct-to-origin traffic. Whether NPM's Advanced config can express ssl_verify_client on; durably was not established in #285.

Out of scope

  • Moving hosting off the NAS. Considered and rejected on 2026-09-05: the driver is origin hiding and cost, and both are served by keeping the NAS as origin.
  • Anything paid beyond the domain registration itself.
  • Phishing and brand-impersonation protection, which #285 established is a paid upper-tier product.

Done when

  • A domain we control resolves to the storefront through Cloudflare, with the record proxied.
  • dig against the public record returns Cloudflare, not the house.
  • trust proxy is corrected and rateLimit.ts:62-64 says something true, shipped in the same change.
  • The DDNS updater writes proxied: true on every update, with something checking it stays that way.
  • The old hostname still answers, with an agreed date at least 30 days out for retiring it.
  • QA and Gitea have been decided deliberately and the decision is written down.
  • docs/ops/cloudflare-free-tier.md is updated to reflect what was actually adopted, rather than being left as a spike that recommended not adopting.

Refs #285

Follow-on from #285, which established that Cloudflare's free tier **cannot be applied to `bermudalamb.synology.me` at all** — the free plan supports only full setup, which needs nameserver delegation for the zone, and the zone is Synology's. Partial (CNAME) setup is Business/Enterprise. The findings are in `docs/ops/cloudflare-free-tier.md`. So origin hiding is blocked behind owning a domain. This issue is that migration. **This is not a DNS change.** The hostname is embedded in outstanding emails, session cookies and signed links, and the application's proxy assumptions are wrong the moment a second proxy exists. Most of the work below is not Cloudflare configuration. ## Scope Register a domain we control, put its zone on Cloudflare, proxy the storefront through it, and keep the NAS as the origin behind DDNS. ## The registrar decision The registrar does nothing for origin hiding on its own — only the Cloudflare proxy does. Buying a domain and using the registrar's own DNS publishes the house IP exactly as today. The registrar's whole job is to own a zone we can delegate to Cloudflare, cheaply. | Registrar | .com renewal | WHOIS privacy | 5-year | Notes | | --- | --- | --- | --- | --- | | **Cloudflare Registrar** | ~$9.77 | Free | **~$49** | At-cost. **Must** use Cloudflare nameservers | | Porkbun | ~$11 | Free | ~$55 | NS delegatable anywhere | | Namecheap | ~$15 | Free | ~$75 | NS delegatable anywhere | | GoDaddy | $22.99 (yr 1 $0.99) | **+$9.99/yr** | ~$122 | Teaser pricing; privacy is a paid add-on | **Proposed: Cloudflare Registrar.** Cheapest, and its one restriction — no external nameservers — is the thing we actually want here. The trade-off is lock-in: leaving Cloudflare later means transferring the domain out, not repointing NS. **Porkbun** is the hedge if that flexibility is wanted, at about a dollar a year more. Prices are from third-party comparison sites as of 2026-09-05, not each registrar's own pricing page. Confirm at purchase. ## Two things that must ship in the same change as the DNS cutover ### 1. `trust proxy` is wrong the moment Cloudflare is in front `app.ts:34` sets `app.set('trust proxy', 1)`. Today the chain is `client → NPM → app` and `req.ip` is the real client. Cloudflare **appends** to `X-Forwarded-For` rather than replacing it, so the chain becomes `client → Cloudflare → NPM → app` and `req.ip` resolves to a Cloudflare edge address. Four limiters key on `req.ip` and would silently stop distinguishing callers while continuing to look healthy — one abusive client would spend everybody's allowance: - `passwordResetRequestLimiter` (via `keyByCallerAndEmail`) - `clientErrorLimiter` (no `keyGenerator`, so it defaults to IP) - `intakeViewLimiter` - `intakeSubmitLimiter` `verificationResendLimiter` is unaffected; it keys on customer id. Fix is `trust proxy: 2`, or read `CF-Connecting-IP` and validate the peer against Cloudflare's published ranges. **The comment at `rateLimit.ts:62-64` asserts the current behaviour as fact and has to change in the same commit**, or it becomes a lie that the next person will believe. This is latent for *any* second proxy, not only Cloudflare. ### 2. The `proxied` flag is a silent-failure trap The home IP is dynamic, so a DDNS updater must keep the Cloudflare A record current via their API. That API call carries a `proxied` boolean. **If the updater ever writes `proxied: false`, the origin IP goes public and nothing breaks.** The site keeps serving, so there is no signal — the protection is simply gone. Same shape as the `trust proxy` problem: correct-looking behaviour, absent protection. Requirements: - The updater must assert `proxied: true` on every write, never inherit it. - Something should periodically verify the record is still proxied and complain if it is not. An unmonitored boolean protecting the thing this whole migration exists for is not good enough. One upside: with a proxied record, visitors always terminate at Cloudflare, so an origin IP change needs no client-side DNS propagation. Strictly better than plain DDNS today. ## The migration surface Verified in the code rather than guessed: - **`docker-compose.prod.yml:191`** hardcodes `PUBLIC_URL=https://redefined-designs.bermudalamb.synology.me`. - **Eight backend files build customer-facing URLs from `PUBLIC_URL`**: `envValidation.ts`, `favoriteAlerts.ts`, `intake/actionLinks.ts`, `intake/notifyDraft.ts`, `routes/adminUploadLinks.ts`, `routes/customers.ts`, `server.ts`, `utils.ts`. - **`UPLOADS_BASE_URL`** is a separate origin and needs deciding alongside. - **`docker-compose.qa.yml`** carries the QA hostname in its documented env block. - **NPM's authentik forward-auth** gates `^/(admin|api/admin)`. That config is not in this repo and has to move with the hostname. Per decision 5 in `project-context.md`, do not widen or narrow that regex in the process. - **Gitea** (`gitea.bermudalamb.synology.me`) resolves to the same address and the Actions runner needs it. Left unproxied it republishes the IP; proxied, Bot Fight Mode may challenge the runner. ## The cutover window Outstanding links and cookies already in customers' hands, with the values checked: | Thing | Lifetime | Consequence of the old host going away | | --- | --- | --- | | Intake action links (`ACTION_TTL_MS`) | **30 days**, hardcoded | Approve/reject links in admin mail stop working | | Session cookie (`SESSION_DAYS`) | 30 days | Cookies are host-scoped — **every customer is logged out at cutover regardless** | | Email verification (`verify_token_hours`) | 24 h default, admin-configurable | Pending verifications break | | Password reset (`password_reset_hours`) | 1 h default, admin-configurable | In-flight resets break | **So the old hostname has to keep answering for at least 30 days after cutover**, or outstanding intake action links die. The logout is unavoidable and worth deciding whether to announce. Upload-link expiry was **not** established — `adminUploadLinks.ts` bounds submissions with `max_submissions` and no expiry was found. Confirm before cutover. ## Decisions this issue needs to make - Domain name, and registrar (proposed: Cloudflare Registrar). - Whether QA goes through Cloudflare too. **If it stays direct it publishes the same address and defeats the exercise**, so "leave QA alone" is not a neutral default. - Whether Gitea is proxied, accepting the Bot Fight Mode risk to the Actions runner, or left exposed. - Bot Fight Mode on or off. #285 found it **cannot be skipped by WAF or Page Rules on any plan** — it does not run on the Ruleset Engine — and Cloudflare documents that it may challenge API traffic. A challenged `POST /webhooks/paypal` is a capture notification we never receive. Safe answer is off. - Whether to pursue Authenticated Origin Pull, which is on the free plan and is the only piece that actually stops direct-to-origin traffic. Whether NPM's Advanced config can express `ssl_verify_client on;` durably was not established in #285. ## Out of scope - Moving hosting off the NAS. Considered and rejected on 2026-09-05: the driver is origin hiding and cost, and both are served by keeping the NAS as origin. - Anything paid beyond the domain registration itself. - Phishing and brand-impersonation protection, which #285 established is a paid upper-tier product. ## Done when - A domain we control resolves to the storefront through Cloudflare, with the record proxied. - `dig` against the public record returns Cloudflare, not the house. - `trust proxy` is corrected and `rateLimit.ts:62-64` says something true, shipped in the same change. - The DDNS updater writes `proxied: true` on every update, with something checking it stays that way. - The old hostname still answers, with an agreed date at least 30 days out for retiring it. - QA and Gitea have been decided deliberately and the decision is written down. - `docs/ops/cloudflare-free-tier.md` is updated to reflect what was actually adopted, rather than being left as a spike that recommended not adopting. Refs #285
Author
Owner

Domains registered — the prerequisite is met

Registered at name.com on 2026-09-06:

Domain Domain Safe Pro
redefined-designs.com Yes
redefined-designs.net Yes
redefined-designs.info Yes
redefined-designs.org No

This settles the blocker this issue was filed for. The registrar comparison above is superseded — the choice was made and the domains are bought, so the Cloudflare Registrar proposal in the issue body is now historical rather than a recommendation. name.com is a full registrar with nameserver control, which is all the Cloudflare free plan's full setup requires.

Domain Lock Plus blocks the nameserver change

This is the part that will bite during the cutover. Domain Safe includes Domain Lock Plus, which by design prevents changes to the domain's nameservers, alongside blocking external transfers and contact/WHOIS changes. It pairs with the account's two-step verification.

Delegating to Cloudflare is a nameserver change, so the cutover sequence has an extra step at the front and back:

  1. Disable Domain Lock Plus on the primary domain (needs 2FA).
  2. Point the nameservers at Cloudflare and let the zone go active.
  3. Re-enable Domain Lock Plus.

Re-enabling afterwards costs nothing operationally: once the zone is delegated, ongoing DNS changes — including the DDNS updater rewriting the A record — happen at Cloudflare, not at name.com, so the lock never sits in the way again.

Two consequences of that same fact, worth knowing rather than discovering:

  • Domain Safe's DNS monitoring stops being useful after delegation. It watches records at name.com; once Cloudflare is authoritative there is nothing there to watch. It will not tell us if the proxied flag gets flipped — that still needs its own check, per the trap described in the issue body.
  • name.com's bundled free SSL certificate is redundant with Cloudflare Universal SSL. Nothing to do, just do not spend time wiring it up.

.org has no Domain Safe Pro

Three of the four carry it and .org does not. Deliberate or an oversight? It matters less than it looks — the lock's value is concentrated on whichever domain actually serves the site — but if .org is meant to be defensive-only it is also the one an attacker would find least protected, so it is worth a conscious answer rather than leaving it as an accident.

Decisions this unblocks, and now needs

  • Which domain is primary. Presumably redefined-designs.com. That value becomes PUBLIC_URL and propagates into every verification email, password-reset link, upload link and intake action link, so it is not a decision to revisit later — see the migration surface in the issue body.
  • What the other three do. Redirect to the primary, park them, or leave them unused. Only the primary needs to go on Cloudflare; adding all four as zones multiplies the configuration for no gain unless they are actually serving redirects.
  • Whether QA gets a hostname under the new domain (qa.redefined-designs.com, say) or stays on the Synology DDNS name. The issue body already flags that leaving QA on the old address republishes the origin IP and defeats the exercise — now there is a domain to move it to.

Everything else in the issue body still stands, in particular the two items that must ship in the same change as the DNS cutover: the trust proxy: 1 → 2 correction with rateLimit.ts:62-64, and asserting proxied: true on every DDNS write.

## Domains registered — the prerequisite is met Registered at **name.com** on 2026-09-06: | Domain | Domain Safe Pro | | --- | --- | | `redefined-designs.com` | Yes | | `redefined-designs.net` | Yes | | `redefined-designs.info` | Yes | | `redefined-designs.org` | **No** | This settles the blocker this issue was filed for. The registrar comparison above is superseded — the choice was made and the domains are bought, so the Cloudflare Registrar proposal in the issue body is now historical rather than a recommendation. name.com is a full registrar with nameserver control, which is all the Cloudflare free plan's full setup requires. ## Domain Lock Plus blocks the nameserver change **This is the part that will bite during the cutover.** Domain Safe includes **Domain Lock Plus**, which by design prevents changes to the domain's nameservers, alongside blocking external transfers and contact/WHOIS changes. It pairs with the account's two-step verification. Delegating to Cloudflare *is* a nameserver change, so the cutover sequence has an extra step at the front and back: 1. Disable Domain Lock Plus on the primary domain (needs 2FA). 2. Point the nameservers at Cloudflare and let the zone go active. 3. Re-enable Domain Lock Plus. Re-enabling afterwards costs nothing operationally: once the zone is delegated, ongoing DNS changes — including the DDNS updater rewriting the A record — happen **at Cloudflare**, not at name.com, so the lock never sits in the way again. Two consequences of that same fact, worth knowing rather than discovering: - **Domain Safe's DNS monitoring stops being useful after delegation.** It watches records at name.com; once Cloudflare is authoritative there is nothing there to watch. It will not tell us if the `proxied` flag gets flipped — that still needs its own check, per the trap described in the issue body. - **name.com's bundled free SSL certificate is redundant** with Cloudflare Universal SSL. Nothing to do, just do not spend time wiring it up. ## `.org` has no Domain Safe Pro Three of the four carry it and `.org` does not. Deliberate or an oversight? It matters less than it looks — the lock's value is concentrated on whichever domain actually serves the site — but if `.org` is meant to be defensive-only it is also the one an attacker would find least protected, so it is worth a conscious answer rather than leaving it as an accident. ## Decisions this unblocks, and now needs - **Which domain is primary.** Presumably `redefined-designs.com`. That value becomes `PUBLIC_URL` and propagates into every verification email, password-reset link, upload link and intake action link, so it is not a decision to revisit later — see the migration surface in the issue body. - **What the other three do.** Redirect to the primary, park them, or leave them unused. Only the primary needs to go on Cloudflare; adding all four as zones multiplies the configuration for no gain unless they are actually serving redirects. - **Whether QA gets a hostname under the new domain** (`qa.redefined-designs.com`, say) or stays on the Synology DDNS name. The issue body already flags that leaving QA on the old address republishes the origin IP and defeats the exercise — now there is a domain to move it to. Everything else in the issue body still stands, in particular the two items that must ship in the same change as the DNS cutover: the `trust proxy: 1 → 2` correction with `rateLimit.ts:62-64`, and asserting `proxied: true` on every DDNS write.
Author
Owner

Correction on .org

The previous comment asked whether .org lacking Domain Safe Pro was deliberate or an oversight. It is neither — name.com does not appear to offer it for .org, so there was nothing to choose.

Worth being honest about the evidence: name.com's public documentation for Domain Safe does not publish a supported-TLD list at all, so this is an observation from the account rather than something confirmed against their docs. If it ever matters enough to rely on, their support is the only authoritative answer.

What that actually costs, and the mitigation

Domain Lock Plus restricts five things: external transfer to another registrar, internal transfer between name.com accounts, toggling WHOIS privacy, changes to contact/WHOIS records, and changes to nameservers.

Without it, .org falls back to the standard registrar lock (clientTransferProhibited), which is free, supported everywhere, and is the mechanism that actually blocks an unauthorised transfer. The gap is therefore narrower than the product tiers suggest — what is lost is the 2FA-gated second layer and the protection against nameserver changes, not transfer protection outright.

Two things to confirm rather than assume:

  1. The standard registrar lock is on for redefined-designs.org. It is on by default at most registrars, and it is the part that matters.
  2. Account-level 2FA is on. With Domain Lock Plus unavailable on this one, the account password becomes the weakest link for it specifically.

Why this is low-stakes here

.org will not serve the site — the primary carries PUBLIC_URL and everything derived from it. The realistic exposure is someone taking .org and standing up a lookalike storefront to phish customers.

That risk exists regardless of this lock, and #285 already established it is not addressable on Cloudflare's free tier — brand protection and phishing takedown are a paid upper-tier product. So the defensive registration is doing the useful work here by denying the name to someone else; the lock tier on it is a second-order concern.

No change to the cutover plan. The Domain Lock Plus step in the previous comment applies only to whichever domain becomes primary, and that one has it.

## Correction on `.org` The previous comment asked whether `.org` lacking Domain Safe Pro was deliberate or an oversight. It is neither — **name.com does not appear to offer it for `.org`**, so there was nothing to choose. Worth being honest about the evidence: name.com's public documentation for Domain Safe does not publish a supported-TLD list at all, so this is an observation from the account rather than something confirmed against their docs. If it ever matters enough to rely on, their support is the only authoritative answer. ## What that actually costs, and the mitigation Domain Lock Plus restricts five things: external transfer to another registrar, internal transfer between name.com accounts, toggling WHOIS privacy, changes to contact/WHOIS records, and changes to nameservers. Without it, `.org` falls back to the **standard registrar lock** (`clientTransferProhibited`), which is free, supported everywhere, and is the mechanism that actually blocks an unauthorised transfer. The gap is therefore narrower than the product tiers suggest — what is lost is the 2FA-gated second layer and the protection against nameserver changes, not transfer protection outright. Two things to confirm rather than assume: 1. **The standard registrar lock is on for `redefined-designs.org`.** It is on by default at most registrars, and it is the part that matters. 2. **Account-level 2FA is on.** With Domain Lock Plus unavailable on this one, the account password becomes the weakest link for it specifically. ## Why this is low-stakes here `.org` will not serve the site — the primary carries `PUBLIC_URL` and everything derived from it. The realistic exposure is someone taking `.org` and standing up a lookalike storefront to phish customers. That risk exists regardless of this lock, and #285 already established it is **not** addressable on Cloudflare's free tier — brand protection and phishing takedown are a paid upper-tier product. So the defensive registration is doing the useful work here by denying the name to someone else; the lock tier on it is a second-order concern. No change to the cutover plan. The Domain Lock Plus step in the previous comment applies only to whichever domain becomes primary, and that one has it.
Author
Owner

Decided: the primary domain is redefined-designs.com

Settled on 2026-09-08. This is the one that goes on Cloudflare, carries PUBLIC_URL, and becomes the origin behind the proxied record.

That resolves the first of the three decisions listed above. The remaining two are still open:

  • What .net, .org and .info do — redirect to the primary, park, or leave unused. Only the primary needs a Cloudflare zone; adding all four multiplies configuration for no gain unless they are actually serving redirects.
  • Whether QA moves onto the new domain (qa.redefined-designs.com) or stays on the Synology DDNS name. Leaving it on the old address republishes the origin IP and defeats the exercise, so this is not a neutral default.

Worth restating because it constrains the cutover rather than being a detail: PUBLIC_URL propagates into every verification email, password-reset link, upload link and intake action link at the moment those are sent. Intake action links live 30 days, so the old hostname has to keep answering that long after the switch. Picking the primary is therefore the point of no return for links already in customers' hands — which is why it is recorded here rather than left in a chat.

## Decided: the primary domain is `redefined-designs.com` Settled on 2026-09-08. This is the one that goes on Cloudflare, carries `PUBLIC_URL`, and becomes the origin behind the proxied record. That resolves the first of the three decisions listed above. The remaining two are still open: - **What `.net`, `.org` and `.info` do** — redirect to the primary, park, or leave unused. Only the primary needs a Cloudflare zone; adding all four multiplies configuration for no gain unless they are actually serving redirects. - **Whether QA moves onto the new domain** (`qa.redefined-designs.com`) or stays on the Synology DDNS name. Leaving it on the old address republishes the origin IP and defeats the exercise, so this is not a neutral default. Worth restating because it constrains the cutover rather than being a detail: `PUBLIC_URL` propagates into every verification email, password-reset link, upload link and intake action link at the moment those are sent. Intake action links live **30 days**, so the old hostname has to keep answering that long after the switch. Picking the primary is therefore the point of no return for links already in customers' hands — which is why it is recorded here rather than left in a chat.
Author
Owner

This is much cheaper than the issue body says, and the reason is temporary

The body was written for a live shop. Production is not live and has no customers, which removes most of the cost it describes:

Written as Actually, right now
Old hostname must answer 30 days after cutover, for outstanding intake action links Nobody holds one. No window needed.
Every customer is logged out at switchover, host-scoped cookies, worth announcing Nobody to log out. Nothing to announce.
In-flight verifications and password resets break None in flight.
Upload-link expiry needs confirming before cutover No links issued to anyone real.

The remaining work is PUBLIC_URL, the authentik forward-auth config, the Gitea host, and the DNS itself. That is the whole migration surface while nobody is depending on the old address.

This is the cheapest this will ever be, and it gets more expensive the moment the shop opens — every link sent from then on has to keep working.

The one thing that must not ship early

trust proxy: 1 → 2 cannot be merged ahead of the DNS cutover. With only Nginx Proxy Manager in front, trusting two hops makes req.ip resolve to something that is not the client, which breaks the four limiters in the direction that looks healthy. It is only correct once Cloudflare is genuinely in front.

So it ships with the cutover, in the same change, exactly as the body says — not as preparation. The same applies to the rateLimit.ts:62-64 comment that asserts the current behaviour.

Ordered sequence

Steps 1–5 are not code and are yours; 6 is the code change and is mine when you reach it.

  1. Add redefined-designs.com to Cloudflare as a zone (free plan, full setup).
  2. Disable Domain Lock Plus at name.com for that domain — it blocks nameserver changes by design, and needs your 2FA.
  3. Point the nameservers at Cloudflare, wait for the zone to go active, then re-enable Domain Lock Plus. It costs nothing afterwards: from then on DNS changes happen at Cloudflare, not name.com.
  4. Create the A record proxied, pointing at the house IP.
  5. Set up the DDNS updater against Cloudflare's API, asserting proxied: true on every write — never inheriting it. A write that quietly sets it false publishes the origin IP and nothing visibly breaks.
  6. Then, in one change: PUBLIC_URL to the new host, trust proxy to 2 with the rateLimit.ts comment corrected, and the authentik forward-auth config moved.

Still open, and worth deciding before step 1

  • Does QA move too (qa.redefined-designs.com), or stay on the Synology name? Staying republishes the same origin IP and defeats the exercise, so this is not a neutral default.
  • Is Gitea proxied? It resolves to the same address, so leaving it exposed has the same effect. But proxying it puts the Actions runner behind Bot Fight Mode, which cannot be scoped on any plan.
  • Bot Fight Mode on or off. Safe answer is off: it cannot be skipped by WAF or Page Rules, and a challenged POST /webhooks/paypal is a capture notification we never receive.
  • What .net, .org and .info do — redirect, park, or nothing.

Not started

Nothing here is implemented. Steps 1–5 are outside the repository, and step 6 is deliberately not prepared in advance because merging it early is the failure mode described above.

## This is much cheaper than the issue body says, and the reason is temporary The body was written for a live shop. **Production is not live and has no customers**, which removes most of the cost it describes: | Written as | Actually, right now | | --- | --- | | Old hostname must answer **30 days** after cutover, for outstanding intake action links | Nobody holds one. No window needed. | | **Every customer is logged out** at switchover, host-scoped cookies, worth announcing | Nobody to log out. Nothing to announce. | | In-flight verifications and password resets break | None in flight. | | Upload-link expiry needs confirming before cutover | No links issued to anyone real. | **The remaining work is `PUBLIC_URL`, the authentik forward-auth config, the Gitea host, and the DNS itself.** That is the whole migration surface while nobody is depending on the old address. This is the cheapest this will ever be, and it gets more expensive the moment the shop opens — every link sent from then on has to keep working. ## The one thing that must not ship early `trust proxy: 1 → 2` **cannot be merged ahead of the DNS cutover.** With only Nginx Proxy Manager in front, trusting two hops makes `req.ip` resolve to something that is not the client, which breaks the four limiters in the direction that looks healthy. It is only correct once Cloudflare is genuinely in front. So it ships *with* the cutover, in the same change, exactly as the body says — not as preparation. The same applies to the `rateLimit.ts:62-64` comment that asserts the current behaviour. ## Ordered sequence Steps 1–5 are not code and are yours; 6 is the code change and is mine when you reach it. 1. **Add `redefined-designs.com` to Cloudflare** as a zone (free plan, full setup). 2. **Disable Domain Lock Plus** at name.com for that domain — it blocks nameserver changes by design, and needs your 2FA. 3. **Point the nameservers at Cloudflare**, wait for the zone to go active, then **re-enable Domain Lock Plus**. It costs nothing afterwards: from then on DNS changes happen at Cloudflare, not name.com. 4. **Create the A record proxied**, pointing at the house IP. 5. **Set up the DDNS updater** against Cloudflare's API, asserting `proxied: true` on *every* write — never inheriting it. A write that quietly sets it false publishes the origin IP and nothing visibly breaks. 6. **Then, in one change:** `PUBLIC_URL` to the new host, `trust proxy` to 2 with the `rateLimit.ts` comment corrected, and the authentik forward-auth config moved. ## Still open, and worth deciding before step 1 - **Does QA move too** (`qa.redefined-designs.com`), or stay on the Synology name? Staying republishes the same origin IP and defeats the exercise, so this is not a neutral default. - **Is Gitea proxied?** It resolves to the same address, so leaving it exposed has the same effect. But proxying it puts the Actions runner behind Bot Fight Mode, which cannot be scoped on any plan. - **Bot Fight Mode on or off.** Safe answer is off: it cannot be skipped by WAF or Page Rules, and a challenged `POST /webhooks/paypal` is a capture notification we never receive. - **What `.net`, `.org` and `.info` do** — redirect, park, or nothing. ## Not started Nothing here is implemented. Steps 1–5 are outside the repository, and step 6 is deliberately not prepared in advance because merging it early is the failure mode described above.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#313