Skip to content
v26.3

Self-Service Onboarding (Architecture)

Self-service onboarding allows prospective customers to set up a DiKAS cloud instance themselves, without contacting sales — either as a free trial or directly as a paying live instance. This page describes the technical architecture, the configuration, and the security mechanisms. The end-user perspective is described under Online Registration.

Disabled by default (dark rollout)

The public funnel is protected by the SelfServiceSignupEnabled flag and is off by default in the shipped state. As long as the flag is false, all public endpoints return 404. Before activation, the points under Before Activation must be fulfilled.

Overview

The flow consists of a public frontend wizard and a series of anonymous backend endpoints. The actual provisioning runs through the same ILicenseProvisioningService that the portal and trial paths also use.

Area Location
Frontend wizard Route /signup (PublicOnboardingWizardComponent, app dikas-web)
Magic-link confirmation Route /signup/verify
Backend endpoints /api/v1/onboarding/public/* and /api/v1/trial/start
Feature pack Dikas.Features.Licensing

The wizard guides the user through seven steps: PackageTSE (Cloud vs. Swissbit) → Account (email, passwordless via magic link) → Company dataOnline name (subdomain) → Legal & paymentDone (instance URL and login credentials).

Feature Flag SelfServiceSignupEnabled

The flag is a property on the singleton document OperationalConfig (fixed ID operationalconfig) and can only be changed via the database or the admin interface — there is no environment variable for it.

OperationalConfig.SelfServiceSignupEnabled  (bool, Default: false)

Every public endpoint first calls CheckSelfServiceEnabledAsync() and responds with 404 Not Found as long as the flag is false. This keeps the funnel invisible until it is deliberately switched on.

Public Endpoints

All endpoints are [AllowAnonymous] and are protected by the flag as well as the anti-abuse measures.

Method & Path Purpose
POST /api/v1/onboarding/public/orders Create an anonymous draft order (returns a secret order token)
PUT /api/v1/onboarding/public/orders/{id} Update the draft (token-bound)
POST /api/v1/onboarding/public/orders/{id}/reserve-name Reserve the online name for a short time
POST /api/v1/onboarding/public/register Create a passwordless customer, send a magic link
POST /api/v1/onboarding/public/orders/{id}/bind Bind the order to the account verified via the magic link ([Authorize])
POST /api/v1/onboarding/public/payment/set Create a Stripe SetupIntent/PaymentIntent (returns ClientSecret)
POST /api/v1/onboarding/public/payment/webhook Stripe webhook (payment confirmation)
POST /api/v1/onboarding/public/orders/{id}/submit Provision the instance
GET /api/v1/onboarding/public/payment-config Stripe publishable key for the frontend
POST /api/v1/trial/start Direct self-service trial (without an order/portal account)

Security / Anti-Abuse

Since every provisioning run generates real costs (COGS), several independent protection layers are active:

  • Honeypot — a hidden form field; if it is filled in, the request is silently rejected with 202 Accepted (no provisioning).
  • Error limiter per IP — exponential delay, lockout after 10 errors (1 h) or 20 errors (24 h).
  • Provisioning quota per IP and 24 h (MaxPublicProvisionsPerIpPer24h, default 3). It applies both to the public submit and to the trial start, and is not reset on success. If the quota is exceeded, the endpoint responds with 429 (code: SIGNUP_RATE_LIMITED).
  • Email-sending quota per IP and hour (MaxPublicEmailSendsPerIpPerHour, default 5) against magic-link bombing.
  • Magic link instead of classic email confirmation; the click verifies the address and returns a portal JWT, which is used to bind the order.
  • Real client IP — behind the ingress, the source IP is determined via ForwardedHeaders. The trusted networks (KnownNetworks) must be set to the cluster CIDR in the deployment, otherwise the IP quota collapses.

In-memory quotas

The quotas are held in memory (a rolling time window). With multiple instances behind a load balancer, each instance counts on its own; in that case, a sticky session or a shared cache should be provided.

Configuration

The options are loaded from the Onboarding section (OnboardingOptions). All values have sensible defaults in the code; they can be overridden via environment variables using a double underscore.

Key Default Meaning
Onboarding__MaxPublicProvisionsPerIpPer24h 3 Max. provisionings per IP / 24 h (submit + trial)
Onboarding__MaxPublicEmailSendsPerIpPerHour 5 Max. magic-link emails per IP / hour
Onboarding__RequireEmailVerifyBeforeProvision true (Portal) Require email confirmation before provisioning
Onboarding__RequireOnlinePaymentForNonTrial false (Portal) Require a real online payment for non-trial

Payment

Payment runs via Stripe on a central DiKAS account (not per tenant).

Key Meaning
Signup__Stripe__SecretKey Stripe secret key (activates the real payment adapter)
Signup__Stripe__PublishableKey Publishable key for the frontend
Signup__Stripe__WebhookSecret Signing secret for the webhook

Without a SecretKey set, a null adapter is active (dev/test). The OnboardingPaymentMethod values: SepaLastschrift = 1 (SEPA direct debit), StripeSetupIntent = 2 (trial-first, store a payment method), StripePaymentIntent = 3 (pay-first, immediate payment).

Lifecycle & Cleanup

Two background services keep the funnel clean:

Abandoned Orders

OnboardingCleanupService (daily) removes abandoned/unpaid orders that have been inactive for more than 14 days (CleanupAbandonedOnboardingsCommand, configurable) — including orphaned anonymous public drafts (without a bound customer) — and releases their online name reservation again. Provisioned/paid orders remain untouched.

Expired Trials (Dry-Run, Dark)

TrialDeprovisioningService is a service deliberately built to be conservative:

  • It finds expired trials whose grace period (GraceDays, default 30) has elapsed after the trial end (IsTrial = true and EndDate beyond the grace period; converted/paid licenses with IsTrial = false are never captured).
  • In DRY-RUN, it only logs which tenant databases ({DatabasePrefix}_maindb, {DatabasePrefix}_gastrocurrent) a later live run would delete — it deletes nothing.
  • It is fully disabled by default and only runs when TrialDeprovision__Enabled=true.
Key Default Meaning
TrialDeprovision__Enabled false Master switch; false = the service does not run at all
TrialDeprovision__GraceDays 30 Grace period in days after the trial ends

Real DB drop is not implemented

The actual deletion of tenant databases is deliberately not wired up. It requires explicit approval as well as a verified database name mapping, and should be coordinated with the expiry enforcement (trial end → lockout → export).

Audit

Every provisioning run is logged via IAuditLogger and is visible in the operator audit view (GET /api/v1/audit/logs/{date}, role Admin only):

Event Trigger
TRIAL_PROVISIONED Successful trial start (with online name, license ID, term)
TRIAL_CONVERTED_PAID Conversion of a trial to a paid license

The timestamp and client IP come from the request; for anonymous trials, the user is shown as -. Login credentials are not written to the audit log.

Before Activation

Before SelfServiceSignupEnabled is ever set to true:

  • ForwardedHeaders:KnownNetworks set to the real cluster CIDR in the prod deployment (otherwise the IP quota is ineffective).
  • Stripe keys (Signup__Stripe__*) set — otherwise an "immediately live" payment with a real provider would be free.
  • Magic-link sending and /signup/verify fully wired up end to end.
  • Flag activated in OperationalConfig.