# Your own domain (https://lazyfrog.dev/docs/domains)

Paid plans can use your own domain, one per project on Hobby, five on Pro and ten on Business. The domain serves the live site, so it shows your site once the project is published; you can add it before.

A domain counts once whether it's a bare domain with its `www` (`example.com`) or one other address (`portal.example.com`, so each address that opens the app directly counts as one more), and a released one never counts. The portal's **Domains** tab says what counts, for example "2 of 5 domains: example.com (with www), portal.example.com".

## Connect it [#connect-it]

Ask your AI to "connect example.com", or add it in the portal under **Domains**. A bare domain makes `www.example.com` your site's main address. Where your DNS provider can point the bare domain at another address (CNAME flattening or `ALIAS`: Cloudflare, Namecheap, Porkbun, Hostinger, NS1, DNSimple), `example.com` is connected too, with its own certificate, and sends visitors to `www`. A subdomain (`portal.example.com`) connects just that.

LazyFrog shows the records to add at your DNS provider, recognises the provider and links to its help page:

| Record | What it does |
|---|---|
| `TXT` records per hostname | One proves the domain is yours; the certificate ones (usually two, and more may appear within a minute: **Check now** shows them) let HTTPS be ready before traffic moves. Safe to add any time. |
| A `CNAME` to `customers.lazyfrog.app` | Sends visitors to your site. On a bare domain this is an `ALIAS` or "CNAME flattening", depending on the provider. |

Keep your `MX` records: your email isn't affected.

Some providers (GoDaddy, IONOS, OVHcloud, Squarespace, Wix, Network Solutions, DreamHost, Amazon Route 53, Azure DNS) can't point a bare domain at another address by name. There only `www.example.com` is connected: edit the `www` record the provider already has (don't add a second one), and forward the bare domain (301) to `https://www.example.com` using the provider's forwarding, which LazyFrog names. The bare domain answers from the provider's forwarder, so it never shows as live in LazyFrog, and that's expected.

At a provider LazyFrog doesn't recognise, only `www` is connected too. If your provider allows a CNAME or `ALIAS` on the bare domain, connect the bare domain afterwards (**Connect example.com too** in Domains) and it sends visitors to `www`; otherwise forward it as above.

You need a domain you already own, registered with any registrar.

## The steps [#the-steps]

The portal shows each step as it completes: **DNS points here → ownership confirmed → HTTPS certificate → live**. Changes at a provider take a few minutes to an hour.

## What each address does [#what-each-address-does]

Every address of your site has one of three roles, which you change in **Domains** or by asking your AI:

| Role | What visitors get |
|---|---|
| **Main** | Your site's address, exactly one. `www` by default; **Make this the main address** picks another, the bare domain included. Your app knows it as `SITE_URL`. |
| **Sends visitors to the main address** | A permanent redirect (308) that keeps the page and its query. The bare domain and your `lazyfrog.app` address work this way. |
| **Opens the app directly** | The same app answers on this address as it is, with no redirect: your app decides what to show for it. For `portal.example.com`, `api.example.com` and so on. A subdomain you add after the main address starts this way. |

Once the main address is live, the `lazyfrog.app` address and the redirecting addresses send visitors to it.

### For your app: SITE_HOSTS and SITE_COOKIE_DOMAIN [#for-your-app-site_hosts-and-site_cookie_domain]

Beside `SITE_URL`, the live app gets `SITE_HOSTS`: the addresses it answers on as they are, comma-separated. First `SITE_URL`'s, then a main address that isn't live yet (it opens the site as it is until then), then the addresses that open the app directly, pending ones included; never the ones that send visitors elsewhere. Once `www` is live that's `www.example.com,portal.example.com`; before, `yoursite.lazyfrog.app,www.example.com,portal.example.com`. In the preview it is the preview's one address.

Both are set when the app starts, at a publish or a wake: a domain going live doesn't restart your app, so publish again (or let it wake) for `SITE_URL` and `SITE_HOSTS` to follow.

With a domain connected, the live app also gets `SITE_COOKIE_DOMAIN`: the main domain's registrable part (`example.com`, or `example.co.uk` for `www.example.co.uk`), for a sign-in shared by its addresses. It isn't set in the preview or without a domain.

An app with several entry points routes by the request's exact hostname. In Next.js (`proxy.ts` in Next 16, `middleware.ts` before it):

```ts
import { NextResponse, type NextRequest } from "next/server";

// Each entry hostname, exactly, and the folder its pages live under (app/portal, app/api).
// Exact hosts: host.startsWith("portal.") would also catch portal.lazyfrog.app, another site.
const ENTRIES: Record<string, string> = {
  "portal.example.com": "/portal",
  "api.example.com": "/api",
};

export function proxy(req: NextRequest) {
  const host = (req.headers.get("host") ?? "").replace(/:\d+$/, "").toLowerCase();
  const base = ENTRIES[host];
  const path = req.nextUrl.pathname;
  if (!base || path === base || path.startsWith(`${base}/`)) return;
  const url = req.nextUrl.clone();
  url.pathname = path === "/" ? base : `${base}${path}`;
  return NextResponse.rewrite(url);
}

// Skips Next's own files, /api, static files by their extension (icon.svg, public/images,
// robots.txt, sitemap.xml; not any dotted path: /invoices/INV-2026.10 is a page) and the
// generated icons, social images and manifest: rewritten under /portal on an entry host
// they'd 404. Each is anchored, so /icons and /api-docs are pages too. api.example.com/orders
// becomes /api/orders and api.example.com/ is /api (add app/api/route.ts for it), while
// api.example.com/api/orders passes through as it is: both answer.
export const config = {
  matcher: [
    "/((?!_next/|api/|api$|favicon\\.ico$|(?:apple-)?icon\\d*(?:\\.\\w+)?$|(?:opengraph|twitter)-image(?:\\.\\w+)?$|manifest\\.(?:json|webmanifest)$|.*\\.(?:svg|png|jpe?g|gif|webp|avif|ico|txt|xml|webmanifest|css|js|map|woff2?)$).*)",
  ],
};
```

In production `req.url` is the app's own `https://0.0.0.0:<port>`, not the visitor's address: build absolute URLs from the `Host` header or `SITE_URL`.

To try an entry address before its DNS is set up, send its hostname: ask your AI to request the app with the header `Host: portal.example.com`, in the preview (`curl` from the terminal) or, once published, in production. A sign-in shared between addresses can only be checked in production on the real domain, once it is live.

A sign-in shared between `www.example.com` and `portal.example.com` needs its cookie set for the parent domain, `Domain=` `SITE_COOKIE_DOMAIN`; without it, each address signs in separately. Set it only when the request's address is on that domain, never on `lazyfrog.app`, which other sites share. An address under another domain of yours can't share it.

One app serves every address of a project: sending `api.example.com` to a different service or port isn't possible yet.
