Skip to main content

Documentation

Sign-up & onboarding

Connect your own sign-up form to the platform — register a user, verify their email, and pick a plan and region with @egav/signup. No login page of ours.

Sign-up in your own UI

Sign-up is a public Control Plane surface — a handful of unauthenticated POST and GET endpoints under /v1/control-plane/public/*. Your customers register on your site, in your branded form; they never see a login page of ours. The @egav/signup client wraps those endpoints so you call typed methods instead of hand-rolling fetch.

This is the first step of the customer journey — register a user, verify their email, then move them into a plan (see Subscriptions & billing) and a portal (see Customer portal).

@egav/signup is a client-only package — no React, no components, no runtime dependencies. It attaches no bearer by default, because these endpoints are public.

Install

Published as @djokodonev/egav-signup-frontend on GitHub Packages, consumed under the alias @egav/signup. Point the @djokodonev scope at the registry with an auth token, then reference the package through an npm alias so imports still read @egav/signup.

.npmrcini
@djokodonev:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
package.jsonjson
{
  "dependencies": {
    "@egav/signup": "npm:@djokodonev/egav-signup-frontend@^0.1.0"
  }
}
GitHub Packages is a private, restricted registry — a bare npm install from the public npm index will not resolve it; the scoped .npmrc above is required. In a monorepo working copy you can instead use a file: dependency ("@egav/signup": "file:../egav-signup-frontend").

Register a user

createSignupClient({ baseUrl }) takes only the Control Plane base URL. signUpWithEmail(...) posts an email and password (plus optional plan, region and business details) and returns whether verification is required.

signUp.tsts
import { createSignupClient } from '@egav/signup';

const signup = createSignupClient({ baseUrl: 'https://cp-api.example.com' });

async function onSubmit(form: { email: string; password: string }) {
  const res = await signup.signUpWithEmail({
    email: form.email,
    password: form.password,
    planCode: 'pro',          // optional — pre-selected from your pricing page
    dataRegion: regionCode,   // optional — a code from the platform's region list
    tosVersion: 'v3',         // optional — the terms version they accepted
    billingCountry: 'DE',     // optional — ISO 3166-1 alpha-2
    captchaToken,             // optional — validated server-side only when enabled
  });

  // res.status is "verification_sent" or "account_created".
  if (res.status === 'verification_sent') showCheckYourEmail(res.message);
}

The input fields (camelCase in the client, snake_case on the wire):

FieldRequiredNotes
email / passwordyesPassword is 8–128 chars (enforced server-side).
firstName / lastNamenoThe form collects the minimum; profile fields come later in onboarding.
planCodenoPre-selected plan from your pricing page.
dataRegionnoA region code from the platform's region list (GET /v1/control-plane/public/regions); must match the Control Plane's platform region.
tosVersionnoThe terms version accepted at sign-up.
billingCountrynoBusiness jurisdiction (ISO 3166-1 alpha-2); validated against the region.
captchaTokennoValidated only when captcha is enabled.
marketingContextnoOpaque utm_* / ref / promo params, preserved through the verify step.

Verify the email

When verification is required, the platform emails a link carrying a token. Your verification page reads the token and calls verifyEmail, which returns a portal redirect URL for auto-login.

verify.tsts
const params = new URLSearchParams(window.location.search);
const result = await signup.verifyEmail(params.get('token')!);

// result.status is "verified" or "already_verified".
if (result.redirect_url) window.location.assign(result.redirect_url);

Need to resend? await signup.resendVerification('dana@acme.io').

Helpers for the form

Three more calls make a good sign-up form:

  • getPlans() — the public, purchasable plan catalog, to render the plan picker. Plans are defined in the Control Plane — your manifest only declares the features a plan can bind (see Plans, features & quotas); this returns the ones a customer can actually buy.
  • checkSubdomain(name) — whether a workspace subdomain is free, for a live-availability check.
  • getPlanAllowedRegions(planCode) — the per-plan region policy, so the region picker can lock disallowed regions. null means no restriction; [] means no choice (hide the picker); a list is the curated subset.
pickers.tsts
const plans = await signup.getPlans();
const { available } = await signup.checkSubdomain('acme');
const { allowed_region_codes } = await signup.getPlanAllowedRegions('pro');

Errors

Every failure throws a SignupError carrying the HTTP status and, when present, the platform envelope errorCode — so you branch on a specific failure instead of matching the message.

errors.tsts
import { SignupError } from '@egav/signup';

try {
  await signup.signUpWithEmail({ email, password });
} catch (err) {
  if (err instanceof SignupError && err.status === 409) {
    setError('That email is already registered.');
  }
}

Endpoints

MethodEndpoint
signUpWithEmailPOST /v1/control-plane/public/signup/email
verifyEmailPOST /v1/control-plane/public/verify-email
resendVerificationPOST /v1/control-plane/public/resend-verification
checkSubdomainGET /v1/control-plane/public/subdomain/check
getPlansGET /v1/control-plane/public/plans
getPlanAllowedRegionsGET /v1/control-plane/public/plans/{code}/allowed-regions

Because these are public, no bearer is needed. The whole authentication story — the four route tiers, why your customers never see an IdP page, and the five gates that protect your own endpoints — is in Auth & the five gates.