Skip to content

Provisioning a new site

Every client site is its own GitHub repository created from the portal template. The template ships pre-wired with the correct pnpm configuration, dev patches, and a postinstall hook that links authoring skills into Claude Code.

There are two ways to provision a site:

  • The admin app (apps/admin) — a one-form hosted flow that creates and wires up everything (repo, Netlify site, subdomain, Supabase records). Recommended for Drawn Agency–hosted clients.
  • Manually from the template — clone the template and configure it yourself. Use this for self-hosted clients, or when you’re not running the admin app. These are the steps further down this page.

The admin app (apps/admin) is the hosted provisioning path: an operator fills in one form and the app creates and wires up the entire site. Access to the app is gated by Supabase auth plus platform-membership checks.

From the admin dashboard, choose Create site (/sites/create). The form collects:

  • Site name — e.g. “Acme Corp Brand Guide”
  • GitHub organization — chosen from the orgs where the Portal GitHub App is installed
  • Repository name — defaults to {slug}-portal
  • Subdomain — the site goes live at {slug}.drawn.guide

On submit, provisionSite() (apps/admin/src/lib/provisioner.ts) runs these steps in order:

  1. Check the subdomain is available in Cloudflare DNS for the drawn.guide zone.
  2. Create a private GitHub repo from the template (configured via TEMPLATE_OWNER/TEMPLATE_REPO) using a short-lived Portal GitHub App installation token.
  3. Create a Netlify site linked to the repo (build command astro build, publish dist).
  4. Create Cloudflare DNS CNAMEs ({slug} and www.{slug} → the Netlify host) and add the custom domain to the Netlify site.
  5. Insert Supabase records — a sites row, the operator as owner in site_users, and a hashed platform API key. The default “Client” viewer audience is seeded automatically by a database trigger (not by the app).
  6. Set the Netlify env vars on the new site — including SITE pinned to https://{slug}.drawn.guide, the Supabase keys, GITHUB_OWNER/GITHUB_REPO/GITHUB_BRANCH, PLATFORM_API_URL/PLATFORM_API_KEY, PORTAL_SITE_ID, a freshly generated per-site SESSION_SECRET, and NETLIFY_WEBHOOK_SECRET — and register deploy webhooks.
  7. Commit the customized config (portal.config.mjs, src/content/site-config.json, src/content/index.json — site name and siteId) to the new repo. This commit triggers the first successful Netlify build. (The build that fires when the Netlify site is first created fails by design — it runs before the env vars exist.)

When it finishes you get a live site at https://{slug}.drawn.guide, the GitHub repo, and a link to the Netlify dashboard. The operator’s existing admin login is the site owner — no new credential is issued; invite additional owners/editors later from the per-site page.

  • The target GitHub org must have the Portal GitHub App installed, or it won’t appear in the organization list.
  • The admin app must be configured with its own environment: the GitHub App credentials, NETLIFY_API_TOKEN/NETLIFY_TEAM_SLUG, TEMPLATE_OWNER/TEMPLATE_REPO, CLOUDFLARE_API_TOKEN/CLOUDFLARE_ZONE_ID, and the shared SUPABASE_* keys. (These are the admin app’s env vars, separate from a client site’s.)
  • Set DEV_DRY_RUN=true to simulate provisioning — it only inserts the Supabase rows, with no real GitHub/Netlify/Cloudflare changes.
  • There is no automatic rollback: if a step fails partway, earlier-created resources (repo, Netlify site, DNS) remain and must be cleaned up manually.

The rest of this page covers provisioning manually from the template.

Go to drawn-agency/portal-template on GitHub and click Use this template → Create a new repository. Choose the client’s GitHub org or account as the owner.

Then clone the new repository locally:

Terminal window
git clone git@github.com:<owner>/<repo>.git
cd <repo>

1. Set the package name. Open package.json and change the "name" field to match the client repo name:

{
"name": "acme-portal",
...
}

2. Set the site display name. Open portal.config.mjs and update site.name:

export default defineConfig({
...
site: { name: "Acme Brand Portal" },
});

3. Fill in environment variables. Copy the example file:

Terminal window
cp .env.example .env

Then open .env and fill in your credentials. See Environment variables for what each value does.

Terminal window
pnpm install
pnpm dev

The dev server starts at http://localhost:4321.

The template repository ships with several pieces of configuration that client sites must not remove:

  • .npmrc — sets shamefully-hoist=true so pnpm hoists transitive dependencies (atlaskit, tiptap, etc.) to the project root where Vite’s optimizer can find and pre-bundle them. Without this, dev mode fails with missing module errors.
  • patches/bind-event-listener@3.0.0.patch — adds an ESM entry point to this CJS-only package (a transitive dependency of atlaskit). Required for dev mode.
  • .portal/ in .gitignore — the Vite plugin auto-generates this directory of symlinks on dev start; it must not be committed.
  • postinstall script — runs node node_modules/@drawnagency/authoring/scripts/link-skills.mjs after every pnpm install, linking the portal authoring skills (including /populate-site) into .claude/skills/.

With the site running locally, continue with: