Signup Customization
Intelligence Engine for self-service signup — configurable onboarding questions with branching logic, option-level visibility conditions, and declarative recommendation rules that map user answers to the right edition.
The Signup Customization system is SCRIPE's Intelligence Engine for the self-service signup flow. Platform administrators define a question tree, each answer option carries a signal weight, and declarative RecommendationRules map answer patterns to specific editions — automatically guiding users to the plan best suited to their needs.
What Is the Intelligence Engine?
The Intelligence Engine is the recommendation system behind SCRIPE's self-service signup. Instead of presenting a static pricing table, users answer a short onboarding questionnaire. The engine matches their answers against RecommendationRules and presents a personalized edition recommendation with a localized reason. Administrators configure questions, options, and rules without code changes.
Signup Flow Overview
The full signup flow from category selection through recommendation.
OnboardingQuestion Entity
Each OnboardingQuestion represents a single step in the onboarding flow. Questions can be global (shown to all users) or scoped to an EditionCategory. Question-level branching is supported via DependsOnQuestionKey + DependsOnAnswerValue.
| Field | Type | Description |
|---|---|---|
| Key | string (max 100) | Stable slug used in code and seeding, e.g. "team_size", "erp_priorities". Must be unique. |
| EditionCategoryId | Guid? | Optional FK to EditionCategory. Null = global (shown to all users). Non-null = only shown for that category. |
| QuestionType | QuestionType enum | SingleSelect (0) or MultiSelect (1). |
| MinSelections | int | Minimum number of options the user must select. Default 1. |
| MaxSelections | int | Maximum number of options the user may select. Default 1 (typically 1 for SingleSelect). |
| IsRequired | bool | Whether the question must be answered before proceeding. Default true. |
| SortOrder | int | Display order within the flow. Lower values appear first. |
| DependsOnQuestionKey | string? (max 100) | Key of a preceding question this question depends on for conditional branching. Null = always shown. |
| DependsOnAnswerValue | string? (max 200) | The answer value from the preceding question that must be selected for this question to appear. |
| LabelEn | string (max 500) | Question text displayed to the user in English. |
| LabelAr | string (max 500) | Question text displayed to the user in Arabic. |
| HintEn | string? (max 1000) | Optional helper/hint text shown beneath the question in English. |
| HintAr | string? (max 1000) | Optional helper/hint text shown beneath the question in Arabic. |
| IconKey | string? (max 100) | Lucide icon name displayed alongside the question (e.g. "users", "target"). See lucide.dev/icons. |
| IsSystem | bool | If true, this question is system-seeded and cannot be deleted by users. |
OnboardingAnswerOption Entity
Each OnboardingAnswerOption is a selectable answer choice for a question. Options carry scoring signals (SignalWeight) for the recommendation engine and optional visibility relevance boosts (RelevanceBoost) that re-rank options based on earlier answers.
| Field | Type | Description |
|---|---|---|
| OnboardingQuestionId | Guid | FK to the parent OnboardingQuestion. |
| Value | string (max 200) | Stable slug used in scoring and branching logic, e.g. "solo", "2-10", "compliance". Unique within the same question. |
| LabelEn | string (max 500) | Primary display label in English (e.g. "Just me", "2–10 people"). |
| LabelAr | string (max 500) | Primary display label in Arabic. |
| SublabelEn | string? (max 500) | Optional secondary/descriptive label in English shown beneath the main label. |
| SublabelAr | string? (max 500) | Optional secondary/descriptive label in Arabic. |
| IconKey | string? (max 100) | Lucide icon name displayed alongside the option (e.g. "user", "users", "building-2"). |
| SortOrder | int | Display order within the option list. Lower values appear first. |
| SignalWeight | int | Integer weight contributed to the edition recommendation score when selected. Default 0 (neutral). Positive = push toward edition; negative = de-prioritize. |
| RelevanceBoost | int | Integer boost applied to this option's ranking when its visibility conditions are satisfied. Default 0. |
Option-Level Visibility Conditions
OnboardingAnswerOptionCondition enables fine-grained client-side visibility control at the individual option level. Unlike question-level branching (which shows/hides entire questions), option conditions show/hide specific answer choices based on earlier answers.
| Field | Type | Description |
|---|---|---|
| AnswerOptionId | Guid | FK to the owning OnboardingAnswerOption. |
| DependsOnQuestionKey | string (max 100) | Stable Key of the preceding question whose answer this condition reacts to, e.g. "scale_healthcare". |
| MatchValuesRaw | string (max 1000) | Backing storage: comma-delimited answer values that satisfy this condition. Accessed via MatchValues computed property. |
| MatchValues | IReadOnlyList<string> (computed) | The set of answer values that trigger visibility. Condition is satisfied if the user's selected values intersect this set (AnyOf / OR semantics). |
| MatchMode | ConditionMatchMode enum | Currently only AnyOf (0) — option visible if user's selection intersects MatchValues. |
SignupSessionAnswer Entity
SignupSessionAnswer persists each user's answer during an in-progress signup. It deliberately uses SignupSessionRef (a plain string) instead of a FK to the SignupSession entity to avoid cross-module coupling — the Entitlements module never imports Identity session types.
| Field | Type | Description |
|---|---|---|
| SignupSessionRef | string (max 256) | Ties the answer to the in-flight SignupSession by its stable Ref string. Avoids a cross-module FK while allowing grouping and resume queries. |
| QuestionKey | string (max 100) | The question this answer belongs to. Matches OnboardingQuestion.Key. |
| ValueJson | string (max 4000) | The submitted answer serialized as JSON. Single-select: "\"solo\"". Multi-select: "[\"compliance\",\"scale\"]". |
| AnsweredAt | DateTime | UTC timestamp of when the user submitted this answer. |
RecommendationRule Entity
RecommendationRules are the declarative matching engine. Each rule defines a ConditionJson predicate, a target edition (by tier level or specific ID), and a ScoreBonus. Rules evaluated in Priority order accumulate scores per candidate edition — the highest-scoring edition wins.
| Field | Type | Description |
|---|---|---|
| Name | string (max 200) | Stable slug used for seeding and upsert operations, e.g. "base-solo-free", "team-compliance-ultra". |
| EditionCategoryId | Guid? | Optional scope to a specific edition category. Null = rule applies across all categories. |
| ConditionJson | string (max 4000) | JSON predicate bag matched against signup answers. Single value: {"team_size":"solo"}. Multi-value (any match): {"priorities":["compliance","security"]}. |
| RecommendedTierLevel | int? | Target tier when no specific edition is pinned. 0 = Free, 1 = Pro, 2 = Ultra, 3 = Enterprise. Null = use RecommendedEditionId instead. |
| RecommendedEditionId | Guid? | Pin recommendation to a specific edition. Takes precedence over RecommendedTierLevel when both are set. |
| ScoreBonus | int | Score added when this rule matches. Higher totals surface the edition higher in the recommendation list. |
| ReasonEn | string (max 1000) | English explanation shown to the user explaining why this edition was recommended. |
| ReasonAr | string (max 1000) | Arabic explanation shown to the user. |
| Priority | int | Tie-break evaluation order. Lower values are evaluated first. |
| IsSystem | bool | If true, this rule is system-seeded and protected from deletion in the admin UI. |
Scoring Algorithm
The recommendation engine evaluates all active rules, accumulates scores, and returns the top-scoring edition with the reason from the highest-priority matching rule.
// 1. Load all active RecommendationRules ordered by Priority ASC
// 2. For each rule, parse ConditionJson and match against SignupSessionAnswers
// 3. Matched rules add ScoreBonus to the candidate edition's total score
// 4. Sort editions by total score DESC
// 5. Return top edition with the highest-priority matching rule's ReasonEn/ReasonArAPI Endpoints
Admin endpoints for question/rule management require entitlements.manage permission. Flow and answer endpoints are public — no authentication required during signup.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/onboarding/questions | List all onboarding questions with their options and conditions | 🔒 Required |
| POST | /api/v1/onboarding/questions | Create a new onboarding question with answer options | 🔒 Required |
| PUT | /api/v1/onboarding/questions/{id} | Update an existing question (label, hint, sort order, branching) | 🔒 Required |
| DELETE | /api/v1/onboarding/questions/{id} | Delete a non-system onboarding question | 🔒 Required |
| GET | /api/v1/onboarding/flow | Get the full onboarding flow for a given category (public - used during signup) | 🔒 Required |
| POST | /api/v1/onboarding/answers | Submit answers for a signup session step (public) | 🔒 Required |
| GET | /api/v1/onboarding/recommend | Get edition recommendation based on submitted answers (public) | 🔒 Required |
| GET | /api/v1/onboarding/rules | List all recommendation rules | 🔒 Required |
| POST | /api/v1/onboarding/rules | Create or upsert a recommendation rule by stable Name slug | 🔒 Required |