Skip to main content

Provider registration

[admin] [signup]

Self-service registration at https://admin.licensio.io/register. Registration creates one free access — no credit card, no plan selection, no portal address. A new provider is ready in under two minutes.

Note: the public marketing site at https://licensio.io (its own Next.js app apps/marketing, separate Vercel deployment, i18n EN/DE with a flag dropdown in the header) links from the header (Sign up) and hero (Start for free →) to https://admin.licensio.io/register?lang=<locale>. The language is carried over into the register form. The contact section opens mailto:malte@licensio.io.

Language on the register page: resolution order is ?lang=en|de (URL param) > LocalStorage licensio_locale > default en. A flag dropdown at the top right allows a manual switch; the selection is persisted in the admin LocalStorage. Form labels, buttons and error messages are all translated.

Lifecycle after registration​

  1. Provider + auth user + tenant_users link are created in /api/register (self-service signup, email confirmed directly). The tenant is created on the free plan, with the accepted terms version + timestamp (terms_accepted_at, terms_version).
  2. No portal subdomain is created at registration. tenants.slug stays NULL until the provider module is activated (App Store, later); only then is a subdomain chosen and provisioned. Until then the provider works entirely in the admin.
  3. Welcome email to the registration address (Resend, from: noreply@licensio.io): greeting by first name ("Hi Malte,") + "Welcome to licensio.io" + CTA to admin.licensio.io. Best-effort (registration does not fail if Resend is down). The login-OTP email (packages/auth/src/email.ts) likewise greets by first name when available.
  4. Redirect to /onboarding. There is no Stripe checkout on the registration path — an upgrade to a paid tier happens later inside the admin under Settings → Subscription.

Forgot password / reset password​

  • /forgot-password (maintenance-exempt): email input → POST /api/auth/forgot-password calls supabase.auth.resetPasswordForEmail(email, { redirectTo: '/reset-password' }). The response is always { success: true } (no info leak).
  • /reset-password (maintenance-exempt): waits for the Supabase PASSWORD_RECOVERY event, then calls updateUser({ password }) client-side. The recovery session comes automatically via the email link; no separate server endpoint needed.

Who it is for​

New providers who want to start with licensio.

Step by step​

1. Fill in the account fields​

The register form asks for exactly these fields — nothing else:

  • First name + Last name: the first two fields (one row, two columns), both required, max 80 characters. Stored on the admin user (tenant_users.first_name / last_name, migration 096) and used to address the provider by first name in the welcome and login-OTP emails ("Hi Malte,"). Editable later under Settings → Advanced → Profile (/settings/advanced/profile).
  • Company name
  • Email: primary login
  • Password: min. 8 characters (+ confirm)
  • Company country: a profile field (tenants.country) used for billing currency and compliance. It does not create a Stripe account and does not set a portal address at this point.
  • Industry (NACE Rev. 2.1): required. A two-level picker (section A–V → division, 87 divisions, DE/EN labels from the official Eurostat source). Up to 3 industries, the first is the main industry. Stored in tenants.industry_codes (migration 093), validated server-side against the nace_divisions reference table.
  • Accept terms + privacy (required checkbox)

There is no plan selection, no tier cards and no shop/subdomain field.

2. Submit​

The backend (/api/register) runs:

  1. Validation (first/last name non-empty + max 80, email format, password length, company name ≥ 3 chars, supported country, at least one valid NACE industry code, terms accepted — see apps/admin/lib/register-validate.ts + validateIndustryCodes)
  2. Cloudflare Turnstile captcha check (fail-open while TURNSTILE_SECRET_KEY is unset)
  3. Email uniqueness check
  4. auth.admin.createUser({ email_confirm: true }): account active directly without mail confirmation
  5. tenants INSERT: free plan, slug: NULL, industry_codes, billing currency derived from the country, default branding (primary_color: #0070F3, button_style: rounded), terms_accepted_at + terms_version
  6. tenant_users INSERT with role='admin', first_name, last_name
  7. Redirect: with provider intent to /onboarding/provider, otherwise /onboarding (post-register OTP e-mail confirmation in between)

On errors the auth user + tenant entry is rolled back.

3. Onboarding wizard​

After a successful registration a wizard guides you through branding, Stripe Connect and the first product:

  1. Welcome: overview of the next steps.
  2. Branding: company name + primary color (logo + further colors later in the dashboard)
  3. Stripe Connect: a click on "Connect Stripe" starts the Express onboarding via /api/stripe/connect/authorize?return=onboarding. After the Stripe setup you land back on /onboarding?step=4&stripe=connected.
  4. First product: direct link to product creation (or skip)
  5. Done: overview of "What's next", link to the dashboard

A progress bar at the top + a step counter at the bottom show the progress. The URL parameter ?step=N jumps to the respective step (e.g. after the Stripe Connect return).

4. Plan gates in the admin​

Features that are not included in the current plan are hidden rather than locked (since 2026-05-29, no more lock banner / 🔒 badge / upgrade CTA):

  • The Widget + Domain tab in settings as well as the associated advanced tiles appear only if the plan has the feature
  • Bulk Import, API, PrintFarm likewise: the nav entry is missing completely without the feature
  • Directly opening a feature page without authorization → 404 (notFound())
  • Feature promotion runs via newsletter + app store (Phase 4), no longer via lock hints

The visibility logic is in apps/admin/lib/plans.ts (isFeatureAvailable + has_* flags). apps/admin/components/PlanGate.tsx renders null when a feature is missing (defensive content layer, future-proof). Backend API routes additionally check the plan server-side (403).

Frequently asked questions​

Which fields does registration ask for? Company name, email, password, company country, industry (NACE Rev. 2.1, required) and the terms checkbox. Nothing else — no plan choice, no tier cards, no subdomain.

When do I get a portal address (<name>.licensio.io)? When the provider module is activated (App Store, later). Only then is a subdomain chosen and provisioned. Admin actions that would send a portal link (invite customer, activate, send mail, resend registration, reset password, newsletter, send notification) are disabled until then.

Can I upgrade to a paid tier? Yes, later inside the admin under Settings → Subscription. Registration always starts on the free access.

Is data handled GDPR-compliant on account deletion? Provider deletion runs via a support request. GDPR Art. 17 is handled by deleting tenant_users + tenants; customer data in deleted_customers is archived (anonymized).