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.
Provision via the admin app
Section titled “Provision via the admin app”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:
- Check the subdomain is available in Cloudflare DNS for the
drawn.guidezone. - Create a private GitHub repo from the template (configured via
TEMPLATE_OWNER/TEMPLATE_REPO) using a short-lived Portal GitHub App installation token. - Create a Netlify site linked to the repo (build command
astro build, publishdist). - Create Cloudflare DNS CNAMEs (
{slug}andwww.{slug}→ the Netlify host) and add the custom domain to the Netlify site. - Insert Supabase records — a
sitesrow, the operator asownerinsite_users, and a hashed platform API key. The default “Client” viewer audience is seeded automatically by a database trigger (not by the app). - Set the Netlify env vars on the new site — including
SITEpinned tohttps://{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-siteSESSION_SECRET, andNETLIFY_WEBHOOK_SECRET— and register deploy webhooks. - Commit the customized config (
portal.config.mjs,src/content/site-config.json,src/content/index.json— site name andsiteId) 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.
Requirements & caveats
Section titled “Requirements & caveats”- 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 sharedSUPABASE_*keys. (These are the admin app’s env vars, separate from a client site’s.) - Set
DEV_DRY_RUN=trueto 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.
Create the repository
Section titled “Create the repository”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:
git clone git@github.com:<owner>/<repo>.gitcd <repo>Customize the project
Section titled “Customize the project”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:
cp .env.example .envThen open .env and fill in your credentials. See Environment variables for what each value does.
Install and run locally
Section titled “Install and run locally”pnpm installpnpm devThe dev server starts at http://localhost:4321.
What the template includes
Section titled “What the template includes”The template repository ships with several pieces of configuration that client sites must not remove:
.npmrc— setsshamefully-hoist=trueso 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.postinstallscript — runsnode node_modules/@drawnagency/authoring/scripts/link-skills.mjsafter everypnpm install, linking the portal authoring skills (including/populate-site) into.claude/skills/.
Next steps
Section titled “Next steps”With the site running locally, continue with:
- Configuration — customize
portal.config.mjsandsite-config.json - Populating content — use
/populate-siteto fill the site with brand content - Deploying to Netlify — connect the repo and go live