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.
@djokodonev:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}{
"dependencies": {
"@egav/signup": "npm:@djokodonev/egav-signup-frontend@^0.1.0"
}
}GitHub Packages is a private, restricted registry — a barenpm installfrom the public npm index will not resolve it; the scoped.npmrcabove is required. In a monorepo working copy you can instead use afile: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.
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):
| Field | Required | Notes |
|---|---|---|
email / password | yes | Password is 8–128 chars (enforced server-side). |
firstName / lastName | no | The form collects the minimum; profile fields come later in onboarding. |
planCode | no | Pre-selected plan from your pricing page. |
dataRegion | no | A region code from the platform's region list (GET /v1/control-plane/public/regions); must match the Control Plane's platform region. |
tosVersion | no | The terms version accepted at sign-up. |
billingCountry | no | Business jurisdiction (ISO 3166-1 alpha-2); validated against the region. |
captchaToken | no | Validated only when captcha is enabled. |
marketingContext | no | Opaque 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.
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.nullmeans no restriction;[]means no choice (hide the picker); a list is the curated subset.
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.
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
| Method | Endpoint |
|---|---|
signUpWithEmail | POST /v1/control-plane/public/signup/email |
verifyEmail | POST /v1/control-plane/public/verify-email |
resendVerification | POST /v1/control-plane/public/resend-verification |
checkSubdomain | GET /v1/control-plane/public/subdomain/check |
getPlans | GET /v1/control-plane/public/plans |
getPlanAllowedRegions | GET /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.