Skip to main content

Custom Domain

Connect a single domain to power branded email (send invoices, quotes, dunning from noreply@acme.sn) and public storefront hosting (visitors browse https://acme.sn instead of https://business.leeram.today/store/acme). Both surfaces, one registration on /settings/domain.

This is the loudest "this isn't really ours" signal customers feel in the product, and the most common upsell trigger when SMEs reach for their first paid tier.

Availability

Currently available on leeram-business deployments (the multi-org SME platform). Email is mediated by kuploy.app's master Brevo account; storefront host-binding runs on the deployment's own Vercel project. You don't configure a Brevo account yourself — sender domain registration, DKIM signing, and bounce handling all happen on kuploy-hub.

For other products (kuploy-cloud, etc.), see SMTP Setup or Email Hosting — those run a separate per-tenant SMTP / Stalwart path.

How it works

your-org owner → /settings/domain

☑ branded email ☑ public storefront

biz → POST kuploy-hub/api/v1/email/domains

┌─ hub: Brevo create-sender-domain (email)
├─ hub: Vercel CNAME at parent zone (both)
└─ biz: Vercel project-domain attach (storefront)

cert provisioning + DNS validation (~15 min)

outgoing mail signs against acme.sn (DKIM)
visitors at https://acme.sn → /store/<orgslug>

You don't see Brevo API keys and never need a Brevo dashboard login. Bounces, complaints, and unsubscribes are handled centrally; reputation pools across all deployments using the master account.

Operator setup

Up to four things on your deployment, depending on what you want to offer:

1. Pick your mail transport

Set on your biz deployment's host (Vercel project envs, or wherever your runtime config lives):

MAIL_PROVIDER=hub      # or "brevo" / "smtp" — see below

MAIL_PROVIDER decides where outgoing transactional mail goes. All three values are first-class options; pick based on whether you want per-org branding or a single deployment-wide sender.

ValuePathSenderPick this when
hubbiz → kuploy-hub → master BrevoPer-org: verified org_domain row → branded; else hub's MAIL_FROM fallbackYou want this feature — per-org branded email
brevobiz → your own Brevo account (BREVO_API_KEY)Deployment-wide MAIL_FROM — no per-org brandingYou want to run your own Brevo and skip hub mediation
smtpbiz → your own SMTP server (SMTP_HOST etc.)Deployment-wide MAIL_FROMYou have an existing SMTP relay you'd rather use
(unset)log-onlyLocal dev / preview without prod creds

For the rest of this guide we assume MAIL_PROVIDER=hub — it's the only path that exercises the per-org org_domain lookup. With brevo or smtp, the registrations on /settings/domain still work (operator can register a domain, hub still verifies DKIM, etc.) but outgoing mail uses your deployment-wide sender, not the org's branded address. You can flip back and forth at any time without losing registrations.

When MAIL_PROVIDER=hub, the license key is read from the synced license_cache row populated by your existing claim flow — no new credential to manage. BREVO_API_KEY on biz becomes unused (hub holds the master key); you can leave it set or remove it.

2. Plan flags for custom domains (paid orgs only)

To let a paid org register a fully custom domain they own (acme.sn, acmetrading.com, etc.), the plan they're subscribed to needs:

  • customEmailDomain: true — branded email on a custom domain
  • customDomain: true — public storefront on a custom domain

The default seed (pnpm db:seed:plans) sets customEmailDomain on Starter+, customDomain on Pro only.

Set on the hub side, per-tenant, in Admin → Tenants → <tenant id> → Free zone:

leeram.today

When set, every org (paid or free) can register <orgslug>.<zone> as their domain. Synced down via license-sync envelope; biz reads it without any local env. Legacy fallback: the env var BRANDED_EMAIL_FREE_ZONE on biz still works if you haven't migrated to the hub-stored value yet.

4. Storefront project-attach — biz VERCEL_PROJECT_ID

For custom-domain storefronts, your biz deployment must tell hub which Vercel project hub should attach the hostname to. Set on your biz deployment:

  • VERCEL_PROJECT_ID — biz's own Vercel project id. Non-secret; just an identifier (e.g. prj_xxxxxxxxxxxxxxxx). Hub uses its own credentials to call POST /projects/:id/domains on your behalf — your biz deployment never holds a Vercel write token.

When VERCEL_PROJECT_ID is unset on biz, the storefront toggle on /settings/domain is disabled; email-only registration still works.

Auto-DNS at the parent zone (and the project-attach itself) are handled hub-side using hub's own credentials and require no setup on your biz deployment beyond this one identifier — kuploy.app manages that side as part of the platform.

Per-surface flow

What happens on Register domain when both surfaces are enabled:

biz POST /api/v1/email/domains  { orgId, domain, usage:"both",
vercelProjectId: $VERCEL_PROJECT_ID }


hub: Brevo create-sender-domain (returns DKIM/DMARC/brevo-code)
├── auto-DNS write Brevo records at parent zone
├── auto-DNS write storefront A record at parent zone
└── attach <domain> to biz Vercel project (cert provision)


hub: insert org_domain { emailStatus: "pending_dns",
storefrontStatus: pending_dns
→ "verified" when DNS + attach both succeeded }


biz post-action license-sync; biz cache picks up the new row


auto-verify cron polls Brevo → emailStatus → "verified"
Vercel cert provisions (~30s) → middleware host-binding active

The org owner's setup

What an organization owner does after you've enabled the feature on their plan:

  1. Open Settings → Domain.
  2. Pick a mode (when both available):
    • Free subdomain — pre-filled with the org slug, e.g. acme-trading-co.leeram.today. No DNS work needed.
    • Custom domain — type your own (acme.sn). Requires the matching plan flag(s) and DNS access at your registrar.
  3. Tick the surfaces you want:
    • Branded email — emails sent as <localPart>@<domain>.
    • Public storefront — visitors reach you at https://<domain>.
  4. Confirm the live sender / storefront preview matches what you want.
  5. Click Register domain.

What happens next depends on whether auto-DNS could write to the parent zone:

Auto-DNS path (free zone, Vercel-reachable): records are written immediately. The cron polls Brevo every 15 min; cert provisioning on the storefront side completes within a minute of DNS propagation. You walk away.

Manual path (custom domain, Vercel doesn't manage your zone): the page shows four FQDN records to publish at your DNS provider:

PurposeTypeSurface
brevo_codeTXTemail
dkim1RecordCNAMEemail (DKIM signing key)
dkim2RecordCNAMEemail (DKIM rotation)
dmarc_recordTXTemail (auth-fail policy)
(storefront)CNAME → cname.vercel-dns.comstorefront

Add them, then click Verify email (or wait for the cron). The storefront cert provisions automatically once DNS resolves.

Verifying it works

A 5-minute end-to-end smoke:

  1. Open /settings/domain as a paid org owner. The form should render with the right toggles enabled per the org's plan.
  2. Walk through registration with a test domain. A free-zone subdomain works fine — acme-test.yourdomain.com.
  3. Email: row flips to emailStatus: verified within 15 min (or click Verify email). Send a test invoice; inspect headers — From: …@acme-test.yourdomain.com, DKIM-Signature: d=acme-test.yourdomain.com, Authentication-Results: dkim=pass.
  4. Storefront: visit https://acme-test.yourdomain.com in a browser. Should serve the org's storefront with a valid TLS cert.

What customers see

After verification, every outgoing message goes out as:

From: Acme Trading <noreply@acme.sn>
DKIM-Signature: v=1; a=rsa-sha256; d=acme.sn; ...
Authentication-Results: dkim=pass d=acme.sn

And every public link the org renders — share buttons on /catalog, the og:url and canonical tags on the storefront page, the View order & pay button on order-confirmation emails, the Open this quote / invoice link in quote and invoice emails, the dunning reminder, sitemap entries — points to the branded origin:

https://acme.sn/                       (storefront)
https://acme.sn/<itemId> (catalog item deep-link)
https://acme.sn/q/<token> (quote share)
https://acme.sn/i/<token> (invoice share)

Storage of these URLs is canonical (${APP_URL}/store/<slug> / /q/<token> / /i/<token>); the swap to the custom origin happens at render time. So if the operator removes the custom domain later, every old display gracefully falls back to the apex — and emails sent yesterday on the branded origin keep their branded links until the row is removed (then the link 404s, same as any expired branded message).

The storefront subdomain flow (acme-trading-co.business.leeram.today) gets the same treatment: middleware passes /q/*, /i/*, /api/*, /_next/*, /robots.txt, /sitemap.xml, and /favicon.ico through unrewritten so token-share routes resolve cleanly on the branded host without needing a per-org /store/<slug> prefix in the email body.

And visitors at https://acme.sn see the org's storefront under their own domain, fully cert-protected — no business.leeram.today in the URL bar.

Replies still go to whoever sent the mail (the user's email for user-initiated sends; not delivered for automated dunning).

Reply-to behaviour

Send typeReply-To
User-initiated (quote / invoice send)The user's own email
Cron-driven (dunning, recurring invoices)Not set — replies bounce

Same as before branded email landed.

Downgrade behaviour

If you downgrade an org's plan to a tier without the matching flags:

  • Outgoing mail from that org immediately falls back to the default sender (your MAIL_FROM, or kuploy-hub's noreply@kuploy.app).
  • The verification record on hub stays intact — re-enabling the feature restores branded sending without a re-verification.
  • The storefront URL stops resolving on the custom host (Vercel cert remains; the lookup just doesn't match a verified row in the synced cache, so middleware doesn't rewrite).

Starting over

If you registered the wrong domain (typo, churned customer), the clean reset is Remove on /settings/domain. Tears down in order: Brevo Sender → Brevo sender-domain → Vercel CNAME records → biz Vercel project-domain detach → org_domain row.

Then re-register from a clean slate. The form preserves the org's display-name default.

Failure modes

What you seeCauseFix
Email status pending_dns for >1hDNS hasn't propagateddig the records to confirm; the cron retries every 15 min
Email status failedBrevo rejected validationCheck DNS values; click Verify email to retry
Storefront URL opens the apex marketing landing pageThe orgDomain row's storefrontStatus is pending_dns, so biz's middleware doesn't rewrite the host. Happens when either auto-DNS or the project-attach failed at register.Check the post-register flash banner — it surfaces the actual reason (auto-DNS skipped / VERCEL_PROJECT_ID missing / project lives in a different team). Fix the underlying cause and Remove + re-register.
Storefront URL returns "Deployment not found"Hub couldn't attach the domain to biz's Vercel projectConfirm VERCEL_PROJECT_ID is set correctly on biz. The post-register flash banner surfaces the actual reason (project not found, in use elsewhere, etc.); if it points to a cross-team setup that needs platform-side action, raise it with kuploy.app support. Click Remove then re-register after fixing.
Storefront URL returns cert errorCert still provisioningWait 1–2 min — Vercel issues Let's Encrypt automatically
Brevo-DNS cleanup partial: removed 0, errors 1 warning on RemoveVercel DNS-records LIST or DELETE returned non-OKHub now logs the actual response ([removeBrevoDnsFromVercel] LIST <zone> failed: <status> <body>). Common causes: the parent zone is no longer in this Vercel team, or the API token's scope changed. Records can be cleaned manually from the Vercel zone editor.
"Couldn't reach the licensing hub" red bannerHub unreachableCheck kuploy.app status
Domain tab missing in SettingsOrg's plan + free-zone combo gates everything offEdit plan in Admin → Plans, or set BRANDED_EMAIL_FREE_ZONE on the tenant

What this doesn't do (yet)

  • Multi-domain per org — one domain per org for v1.
  • Path 1 auto-DNS via registrar API — for domains bought through the platform. Today every domain is BYO-only.
  • Inbound mail / per-org mailboxes — that's Email Hosting, a separate Stalwart-based feature.
  • Per-org reputation isolation — all branded mail shares the master Brevo account's IP reputation.