Skip to content

First-time setup

This page walks the first hour of life as a platform admin, from the moment your host is provisioned to the moment your first real user can sign up and build something.

If you have been an admin elsewhere, scan the headings and skip ahead. The order below is the safest sequence: each step assumes the previous ones worked.

Pre-flight: Provision the host

Before you ever open the dashboard, the host needs to exist. You have two ways to do this:

Either way, the host ends up with the doable systemd service running a tmux session (api, web, ws), Caddy and Cloudflare Tunnel serving HTTPS, and the database migrated.

Required vs optional env vars at install

The installer writes a sensible .env. The minimum that must be set for the platform to function: DATABASE_URL, JWT_SECRET, ENCRYPTION_KEY, WS_URL, API_URL. Everything else (RESEND_API_KEY, STRIPE_SECRET_KEY, the OAUTH_* clients, CLOUDFLARE_API_TOKEN, AI provider keys) is optional and can be added later from /admin (see Step 4, Step 5b, Step 7, Step 8). Full list: Environment Variables.

0. Sign in as the bootstrap admin

The first account that signs up on a fresh install becomes the platform owner. The signup happens through your dashboard URL like any other user; there is no separate root-only console.

Confirm you are the platform owner:

  • Open /admin. If the page renders, you are gated correctly by the usePlatformAdmin() hook in apps/web/src/app/(dashboard)/admin/page.tsx.
  • Open /admin?tab=users and look for your own row. It should carry the crown icon and show platform_role = owner in the Role select.

If /admin 404s on the first account, something went wrong during install. Recover by setting platform_role = 'owner' directly on your users row in the database, then reload.

Tip

The bootstrap owner cannot demote themselves. If you want a backup admin, promote a second account immediately (see Step 1).

0a. Complete the /setup wizard

The very first time the bootstrap owner signs in, Doable redirects to /setup instead of /admin. This is a one-time, five-step wizard gated three ways (apps/web/src/app/setup/WizardShell.tsx:14, page.tsx:31-45):

  1. You must be authenticated.
  2. You must be a platform admin (which the bootstrap owner is, automatically).
  3. The instance's setup_completed_at must be null. Once you finish the wizard, this is set, and nobody (including you) sees the wizard again.

The five steps are designed to be fast (under five minutes). Each step is a deliberate "this is the smallest thing you can configure to make the platform usable"; every step can be skipped and revisited from /admin later. The on-screen step order (WizardShell.tsx:163-180) is:

Step File What it asks
1. Welcome Step1Welcome.tsx Workspace name (saved via POST /setup/workspace-name)
2. Sign-in providers Step3SignInProviders.tsx Google, GitHub, Supabase OAuth client IDs and secrets. Renders ONE GitHub OAuth app for sign-in + Copilot + repo push/pull
3. AI Provider Step2AIProvider.tsx Pick from 60+ providers, a GitHub Copilot account (inline OAuth popup), or a custom OpenAI-compatible URL. A "Use as default for every plan" checkbox is on by default
4. Cloudflare Tunnel StepCloudflare.tsx Detects cloudflared binary, tunnel config, service state; offers a one-line install + login flow or a "skip with warning" path
5. Plans & billing Step4Integrations.tsx Per-plan default AI model (inline PlanDefaultsInline.tsx), signup-approval toggle, optional Stripe secret + webhook secret

The visual order intentionally puts Sign-in before AI Provider so the GitHub OAuth client ID and secret are saved before the Copilot tile triggers its OAuth handshake (WizardShell.tsx:18-22). The numeric mapping is intentional: the component Step3SignInProviders is rendered AT step index 2, and Step2AIProvider is rendered at step index 3.

The Cloudflare step is always shown, never auto-skipped on missing DOMAIN; the operator can explicitly skip with a confirm dialog if they accept the warning about direct port 80/443 exposure. PlanDefaultsInline is an inline component rendered inside Step 5, not a separate step.

If you accidentally close the browser mid-wizard, just re-visit any URL; you'll be redirected back to /setup until setup_completed_at is set. Going back is allowed; the progress bubbles at the top of /setup jump to any completed step.

Why this is separate from /admin

/setup exists so a fresh-install owner can stand up a working platform in five minutes without making 23 micro-decisions. /admin is for ongoing operations. After this wizard, you spend the rest of your platform-admin life in /admin.

1. Promote a backup platform admin

A single owner is a single point of failure. Within the first ten minutes:

  1. Have a trusted second person sign up through the normal dashboard.
  2. Open /admin?tab=users, find their row, change Role from member to admin.
  3. Confirm the row updates and shows the crown icon.

Cross-reference: Every /admin screen → Users & AI. Reference page: operations/platform-admin/users.md.

2. Set instance-wide defaults: feature flags

Open /admin (Feature Flags tab, the default landing tab).

Walk the list of feature flags top to bottom. For each one, decide:

  • Master toggle: on or off platform-wide?
  • Min Plan: restrict to Pro+, Business+, or leave open?
  • Min Role: workspace-Admin only, or open to members?

You can return to this list any time. The point of the first pass is to make sure no surprise features are quietly enabled (e.g. marketplace publishing, custom domains, Provider Bridge BYOK) before any users see them.

Below the flag list you will find the Frameworks section. Pick the default framework new projects should start from. vite-react is the shipping default; if you prefer Next.js App Router, click "Set as default" on that row. At least one framework must remain enabled.

Reference: operations/platform-admin/features.md, operations/platform-admin/frameworks.md.

3. Set the default plan

Open /admin?tab=plans.

There are two sub-tabs here. Visit each.

Plan Limits: for each of Free, Pro, Business, Enterprise, review the eight limits:

  • Max Projects
  • Max Members
  • Daily Credits
  • Monthly Credits
  • Max File Size
  • Custom Domains (toggle)
  • Analytics (toggle)
  • Priority Support (toggle)

The shipping defaults come from PLAN_LIMITS in @doable/shared. If you change anything, the plan card gets a "CUSTOMIZED" amber pill. The Reset button restores the hardcoded default. If you are running a closed beta, consider tightening Free aggressively (e.g. one project, no custom domains) so the cost ceiling is predictable.

Plan Defaults (AI): sets the default AI model and provider that new workspaces on each plan will inherit. You will configure providers themselves in Step 4; come back here after that.

Reference: operations/platform-admin/plan-limits.md.

4. Configure AI providers

Doable supports three AI configuration shapes:

  • Bring your own key (BYOK): each workspace registers its own provider in /ai-settings. Nothing for you to do here except make sure the Provider Bridge BYOK feature flag is on.
  • Provider Bridge: you, the platform, hold the provider credentials. Each workspace gets a clone of those credentials with re-encrypted tokens (pgp_sym_encrypt using ENCRYPTION_KEY). End users see a model dropdown but never touch a key.
  • GitHub Copilot accounts: a Copilot account is allocated to a user; their workspace receives a cloned copy via cloneCopilotAccountToWorkspace() in services/api/src/routes/admin-ai.ts.

For a fresh install, the recommended order is:

  1. Open /ai-settings (your own personal workspace) and add the platform provider(s) there. This is the source-of-truth row that will be cloned to other workspaces.
  2. Return to /admin?tab=plans, Defaults sub-tab. For each plan that should have a default AI:
    • Click Configure
    • Choose GitHub Copilot or Custom Provider
    • Pick the account/provider you just registered, then the model
    • Save
  3. If you have existing workspaces from a previous install, click Apply to Existing. Leave the "Overwrite existing workspace AI settings" footer toggle off unless you really mean to clobber user-set choices.

For deeper context on the Bridge versus BYOK trade-off, see AI → Provider Bridge.

5. OAuth apps: two flavours, configure both

Doable uses OAuth in two completely different places. Get them straight before you start clicking through provider consoles:

Flavour What it does Where it surfaces Where you configure it
Sign-in OAuth Users click "Continue with Google" on /signup or /login to create or authenticate an account The login/signup page itself Step 5a (the wizard touched this; this is the deeper version)
Integration OAuth An end-user grants Doable access to their Slack / Gmail / GitHub / Stripe / etc. so the AI or their app can act on their behalf Workspace Settings → Integrations Step 5b

Same protocol, totally different OAuth apps. Sign-in apps redirect to /auth/callback; integration apps redirect to per-integration callback URLs and ask for very different scopes. Register them separately in each provider's developer console.

5a. OAuth providers for sign-in

Open /admin?tab=users (this surface co-locates with users management because it controls who can be a user).

Doable ships sign-in connectors for Google, GitHub, GitLab, Microsoft, Discord, and email/password. To enable a provider:

  1. Create an OAuth app in the provider's developer console. Callback URL: https://<your-doable-host>/auth/callback. Scopes: minimal, such as openid email profile (Google), read:user user:email (GitHub), etc.
  2. Set client_id and client_secret either in your .env (recommended for sign-in providers, since they rarely change), for example GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID, or paste them into the Doable UI if a UI field exists for that provider.
  3. Open the dashboard in an incognito window. The provider icon should appear on /login. Click it, complete the OAuth round-trip, confirm a new user is created.

Email/password is always on

Even if you only want OAuth sign-in, email/password authentication remains available unless you compile it out. For instance, password-based recovery is the only fallback when an OAuth provider has an outage.

5b. Platform-managed OAuth apps for integrations

Open /admin?tab=integrations.

This is where you register platform-managed OAuth apps so end users can connect to Slack, Gmail, Google Drive, GitHub, Stripe, and so on without registering their own OAuth clients.

For each integration you want to enable instance-wide:

  1. Create an OAuth app in the provider's developer console (Slack API portal, Google Cloud Console, Stripe Dashboard, etc.) with the integration-specific callback URL shown next to the integration in the Doable UI.
  2. Click Add OAuth credentials in Doable; paste the client_id and client_secret.
  3. Set isGlobal = true so every workspace can use it.
  4. Click Connect Test to verify the round-trip works.

Watch the Source badge column on the integration list:

  • Platform app: global OAuth app you added in the UI
  • Workspace app: one workspace registered its own
  • Env-configured: credentials come from OAUTH_<UPPER_ID>_CLIENT_ID / _CLIENT_SECRET env vars (cannot delete from UI; edit the env)
  • Personal: user-level connection, not yours to manage

Google integrations share GOOGLE_INTEGRATIONS_CLIENT_ID / GOOGLE_INTEGRATIONS_CLIENT_SECRET (with GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET as fallback). GitHub integrations share GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET. If you set these in .env during install, they will already show as Env-configured.

Don't conflate the two

Your GitHub sign-in OAuth app (Step 5a) and your GitHub integration OAuth app (Step 5b) are typically different apps in GitHub's developer console: different callbacks, different scopes, different consent screens. Sharing one app between both flows works in development but produces awkward consent dialogs in production.

Reference: operations/integrations-admin.md.

6. Enable MFA enforcement (within the limits of v1)

Open /admin?tab=mfa.

MFA v1 shipped in May 2026 (TOTP + recovery codes, optional only, admin reset). The schema is extensible for WebAuthn and required-policy later, but the UI cannot yet enforce MFA platform-wide.

What you can do today:

  • See who has MFA enabled, when they last used it, and how many recovery codes they have left.
  • Reset MFA for a user who has lost both their authenticator and their recovery codes. The confirm dialog spells out what will happen: every MFA factor and recovery code is cleared, every active session is signed out, the user can log in with just their password until they re-enroll.
  • Communicate an org-wide expectation that all admins enroll in MFA. (Send a broadcast in Step 7.)

Every reset goes into the admin audit log as action = "mfa.reset".

Reference: User Guide → Two-Factor Auth.

7. Set the DNS wildcard for /admin

Open /admin?tab=dns. This is where you configure Cloudflare wildcard DNS automation so the platform can create per-publish subdomains without manual intervention.

The free Cloudflare Universal SSL cert covers <zone> and *.<zone> only. If you are running a multi-level subdomain layout (e.g. *.staging.doable.me), you need Advanced Certificate Manager (ACM); the panel will refuse auto-setup with reason: "free-plan-multilevel" otherwise.

Walk the panel top-down:

  1. Mode toggle: choose wildcard if you want one *.<zone> CNAME serving all published apps. per_publish creates a CNAME per publish.
  2. Diagnostics card: confirm zone name, plan, ACM status, recommended wildcard hostname. canAutoSetup must be true for the next step.
  3. Custom wildcard input: the field pre-fills with the recommended hostname (must start with *., must be inside the zone). Tick Apply ACM override only if you really do have paid ACM. Click Auto Setup.
  4. Existing wildcards list: verify the new record appears with proxied = true. Delete any stale wildcards from earlier installs (two-step confirm).
  5. Cloudflare API token override: your token is KEK-encrypted. The source field shows whether it lives in platform_settings (DB), env, or is missing. Rotate by pasting a new token; remove with the trash button.

If the auto-setup fails, the diagnostics card gives a precise reason enum: no-cf-creds, no-tunnel-id, no-publish-domain, free-plan-multilevel, zone-lookup-failed. Each one tells you exactly what to fix.

8. Email + announcement defaults

Open /admin?tab=email.

You need a working outbound email before signup approvals, MFA emails, and broadcasts deliver.

  1. Provider type: pick SMTP, Resend, or Google.
    • SMTP: choose a known service (Gmail, SendGrid, Mailgun, SES, Postmark, etc.) or Custom (manual SMTP) for host + port + user + pass.
    • Resend: paste the API key.
    • Google: complete the OAuth flow; you will land back with ?gmail=connected and a success toast.
  2. Save the config, then Test email to your own address. Confirm it arrives.
  3. Verify credentials to round-trip-check the connection. If verify fails, the response surfaces the SMTP/HTTP error verbatim.
  4. Queue stats card: watch pending / processing / sent / failed / dead. After your test email, you should see one in sent.

While you are here, do not draft an announcement yet. Wait until the rest of setup is done so the first email you send is meaningful. See every /admin screen → Email for queue browser and retry/delete actions.

9. Verify the first signup flow works

Open /admin?tab=signups.

If you want to gate registrations (typical for a small instance), enable approvals:

  1. Check "Require approval for new signups"
  2. Edit the pending-signup message (max 2000 chars). This is what visitors see after they sign up
  3. Save settings

Now test the flow end-to-end:

  1. Open the dashboard in an incognito window. Sign up with a throwaway email.
  2. Confirm the pending-signup screen appears with your message.
  3. Switch back to /admin?tab=signups. The new user should appear under Pending approvals with the right provider badge (Email, GitHub, or Google), display name, and signup time.
  4. Click Approve. The user gets a personal workspace (ensureWorkspace runs server-side) and a welcome email goes out.
  5. Sign back in as that test user; confirm the dashboard loads.

If anything along that chain fails, the most common causes are:

  • Email provider misconfigured in Step 8 → the pending notification email never arrives
  • Plan defaults not configured in Step 3/4 → the new workspace has no AI and the editor is read-only
  • Feature flag locked down too aggressively in Step 2 → the user can sign in but cannot do anything

Once that flow works, you have a healthy instance. Now read Every /admin screen to learn the rest.