Welcome to Lead Router
Lead Router is a self-hosted lead routing engine for Salesforce. Deploy it to your own VPS with a single CLI command and gain full control over how Leads, Contacts, and Accounts are assigned to your team -- all without your data leaving your infrastructure.
Architecture
Lead Router runs as a Docker Compose stack on your server. When Salesforce records are created or updated, Apex triggers fire and send the record to the routing engine, which evaluates your rules and updates the owner in real time.
What you'll need
app.yourdomain.com and api.yourdomain.com at your VPS IP. SSL is provisioned automatically via Let's Encrypt.
Step 1: VPS & DNS Setup
Provision a server and point two domain names at it.
Provision a VPS
Create a Linux server with any cloud provider. Recommended specs:
| Spec | Minimum | Recommended |
|---|---|---|
| CPU | 1 vCPU | 2 vCPU |
| RAM | 2 GB | 4 GB |
| Disk | 20 GB | 40 GB SSD |
| OS | Ubuntu 22.04 / Debian 12 / CentOS 8+ | |
Popular options: DigitalOcean ($12/mo), Hetzner ($5/mo), Linode ($12/mo), AWS Lightsail ($10/mo).
Create DNS A Records
In your DNS provider, create two A records pointing to your VPS IP address:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | app | your-vps-ip | 300 |
| A | api | your-vps-ip | 300 |
app subdomain serves the web dashboard. The api subdomain serves the routing engine that Salesforce calls. Replace the IP with your actual VPS IP address.
New to DNS?
An A record maps a domain name to an IP address. When someone visits app.acme.com, their browser looks up the A record to find the server's IP.
After adding the records, it can take 5-30 minutes for DNS to propagate. You can check propagation with dig app.acme.com from your terminal.
The CLI will automatically provision SSL certificates via Let's Encrypt once DNS is pointing correctly, so you don't need to worry about HTTPS configuration.
Step 2: Salesforce Connected App
Create an OAuth Connected App so Lead Router can securely access your org.
Step-by-step
- In Salesforce, go to Setup → search "App Manager" → click New Connected App
- Fill in the basic info:
- Connected App Name:Lead Router
- API Name:Lead_Router(auto-filled)
- Contact Email: your email - Under API (Enable OAuth Settings), check "Enable OAuth Settings"
- Set the Callback URL to:
https://app.acme.com/api/auth/sfdc/callback
Replaceapp.acme.comwith your actual app domain - Add these OAuth Scopes:
- Access your basic information (api)
- Perform requests at any time (refresh_token, offline_access) - Check "Require Secret for Web Server Flow"
- Click Save, then wait ~2 minutes for Salesforce to register the app
- Click "Manage Consumer Details" → verify your identity → copy the Consumer Key and Consumer Secret
https:// and the path /api/auth/sfdc/callback. A mismatch will cause a redirect_uri_mismatch error during Salesforce login. Use https://test.salesforce.com as the login URL for sandbox orgs.
What is a Connected App?
A Connected App is Salesforce's way of allowing external applications to authenticate via OAuth 2.0. It provides a Consumer Key (Client ID) and Consumer Secret that Lead Router uses to securely obtain access tokens for your org.
Your credentials are stored exclusively on your server -- Lead Router never sends them to any third party.
Step 3: Run the CLI
One command deploys the entire stack to your server.
Run the installer
Open a terminal on your local machine (not the VPS) and run:
Requires Node.js 20+. The CLI runs on your machine and connects to the VPS via SSH -- nothing is installed locally.
CLI prompts
The wizard will ask for the following information:
lead-routing
The CLI will SSH into your server to deploy the full stack.
You will need:
- Server hostname or IP address
- SSH password for your server (root access)
- A fresh Linux VPS -- Docker installed automatically
-- Server connection -----------------------------
? Server hostname or IP address
165.22.100.50 or vps.acme.com
> 165.22.100.50
? SSH password for root@165.22.100.50
> ************
-- App configuration -----------------------------
? App URL (public URL where the web app will be accessible)
https://routing.acme.com
> https://app.acme.com
? Engine URL (public URL Salesforce will use to route leads)
https://engine.acme.com or https://acme.com:3001
> https://api.acme.com
-- Salesforce Connected App setup -------------------
? Consumer Key (labelled "Client ID" in newer orgs)
> 3MVG9dAEux2v1sLt...
? Consumer Secret (labelled "Client Secret" in newer orgs)
> ****************
-- Admin Account ---------------------------------
? Admin email address
> admin@acme.com
? Admin password (min 8 characters)
> **********
What happens next
The CLI will automatically perform these steps on your VPS:
✔ Node.js v20+ found
✔ Connected to 165.22.100.50 via SSH
✔ Docker detected on remote server
✔ Config files uploaded to ~/lead-routing
◇ Pulling Docker images on server (this may take a few minutes)...
✔ Docker images pulled
◇ Starting services...
✔ Services started (5 containers)
◇ Running database migrations...
✔ Database migrated and seeded
◇ Waiting for health checks...
✔ Web app -- https://app.acme.com
✔ Engine -- https://api.acme.com
◇ Deploying Salesforce package (this may take ~2 min)...
✔ Package deployed (14 components)
✔ Permission set assigned
✔ Lead Router is live!
Dashboard: https://app.acme.com
Login with: admin@acme.com
Services deployed
| Service | Port | Purpose |
|---|---|---|
caddy | 80, 443 | Reverse proxy + automatic HTTPS |
web | 3000 | Management dashboard (Next.js) |
engine | 3001 | Routing engine (Fastify + BullMQ) |
postgres | 5432 | Database |
redis | 6379 | Queue + caching |
Step 4: Onboarding Wizard
Connect your Salesforce org and activate triggers -- all from the Salesforce UI.
Open the wizard
- In Salesforce, click the App Launcher (grid icon, top-left)
- Search for "Lead Router Setup" and click on it
- The onboarding wizard will open inside Salesforce
Complete all 4 wizard steps
- Step 1 -- Connect: Click "Connect to Lead Router" → authorize the OAuth popup → wait for the green checkmark (~5 seconds)
- Step 2 -- Activate Triggers: Click Activate to enable Lead insert/update triggers. This tells Salesforce to send records to the engine whenever they change.
- Step 3 -- Sync Fields: Click Sync Fields to index your Lead field schema. This enables the rule builder to show your custom fields.
- Step 4 -- Test: Click Send Test to fire a test routing event. "Test successful" or "No matching rule" are both valid -- it confirms the connection works.
Step 5: Connect Salesforce
Manage your Salesforce connection and package deployment from the web dashboard.
Salesforce Integration Hub
Navigate to Integrations in the sidebar. This page gives you full control over your Salesforce connection without needing the CLI.
What you can do
- Deploy / Redeploy Package -- Push the latest Apex triggers, classes, and components to Salesforce. No CLI required.
- Sync Fields -- Refresh the field schema from Salesforce so the condition builder shows current fields and picklist values.
- Enable Objects -- Choose which objects (Lead, Contact, Account) are routable.
- View Status -- Check deployment status, last sync timestamp, and connection health at a glance.
- Disconnect -- Revoke the OAuth connection and remove stored credentials.
lead-routing sfdc deploy from the CLI. All Salesforce package management can now be done directly in the web dashboard.
Step 6: Sync Fields
Ensure the route builder has access to your Salesforce field schema.
How field sync works
When you click Sync Fields (either in the onboarding wizard or the Integrations page), Lead Router reads the metadata for all enabled objects (Lead, Contact, Account) and imports:
- Field API names, labels, and data types
- Picklist values for dropdowns in the condition builder
- Required/optional status and relationship references
This is what powers the dropdowns in the route builder's condition editor. If you add new custom fields in Salesforce, re-sync to make them available.
Step 7: Deploy Triggers
Apex triggers are the bridge between Salesforce and the routing engine.
What gets deployed
The Salesforce package includes:
- Apex Triggers --
LeadTrigger,ContactTrigger,AccountTrigger-- fire on insert/update and send record data to the engine. - Apex Classes --
RoutingEngineCallout(HTTP callout),RoutingPayloadBuilder(JSON serialization),CriteriaEvaluator(pre-callout filtering). - Custom Settings --
Routing_Settings__cstores the engine endpoint URL, webhook secret, and toggle flags for each object/event. - Custom Objects --
Route_Criteria__cstores trigger criteria synced from the web app for pre-callout filtering. - Permission Set --
LeadRouterAdmingrants access to the setup app, custom objects, and fields.
Deploying from the web dashboard
Go to Integrations → click Deploy Package. The deployment uses the Salesforce Metadata API and typically completes in 1-2 minutes.
Deploying from the CLI
Run this from the same directory where you ran init. It reads the saved config from lead-routing.json.
Step 8: Complete Setup
Verify everything is working end-to-end before building your first route.
Checklist
- Web dashboard loads at your app URL with HTTPS
- You can log in with the admin email and password from CLI init
- Integrations page shows Salesforce as "Connected"
- Package shows as "Deployed" with all components
- Field sync shows your Lead fields
- "Lead Router Setup" appears in Salesforce App Launcher
Step 9: License Users
Control which Salesforce users can receive routed records.
Why license users?
Only licensed users are eligible to receive routed records. This lets you control exactly which reps participate in routing -- an unlicensed user will never be assigned a lead, even if they're in a team.
Sync users from Salesforce
- In the web dashboard, go to License Users in the sidebar
- Click the "License Users" button in the top-right
- Choose a licensing strategy:
Four strategies are available:
- All Users -- License every user from Salesforce
- Individual Users -- Pick specific users to license
- By Role -- License all users with selected Salesforce roles
- By Profile -- License all users with selected Salesforce profiles
Managing the user table
After syncing, you'll see a table of all imported users. Toggle the switch to license or de-license individual users. Use the search bar and filters to find specific users. Select multiple users for bulk actions.
Step 10: Create Teams
Create pools of reps for lead distribution. Choose between equal round robin (every rep gets the same share) or weighted round robin (assign proportional shares per rep).
Create a team
- In the sidebar, click Teams
- Click "New Team"
- Enter a team name (e.g. "SDR West") and optional description
- Click Create
Add members
- Click Manage on the team card to open the detail page
- Click "Add Members" and choose a method:
- Individual Users -- search and select specific licensed users
- By Role -- add all users with a specific Salesforce role
- By Profile -- add all users with a specific Salesforce profile
Team controls
- Active / Paused -- Toggle a member's status. Paused members are skipped during assignment (e.g. when someone is on PTO).
- Reset Pointer -- Resets the rotation back to the first member. Useful after adding or removing members.
- Share % -- Shows each member's share of total assignments. An even distribution means fair routing.
Distribution types
Each team supports two distribution modes, selectable on the team detail page:
- Equal Round Robin (default) -- Every active member receives leads in strict sequential order, one each in turn. All members get an equal share.
- Weighted Round Robin -- Assign a weight to each member to control what proportion of leads they receive. For example, weights of 40/40/20 mean the first two reps each get 40% and the third gets 20%.
In weighted mode, use the sliders to adjust each member's weight. Toggle between Percentage (must sum to 100) and Points (must sum to 10) modes. Use the Equalize button to reset all weights to equal.
Step 11: Create Routes
Set up routing rules in the web dashboard.
Create a rule
- Log in to the web dashboard at your app URL with the admin email and password you set during CLI init
- Navigate to Routing Rules in the sidebar
- Click New Rule
- Give it a name (e.g. "Enterprise Inbound") and select Lead as the object type
- Set the trigger to Insert (fires when new Leads are created)
- Click Edit Flow to open the visual route builder
Example: Route by company size
In the flow builder, create a simple rule: any Lead where Company contains "Enterprise" gets assigned to the Enterprise SDR team via round robin.
Step 12: Route Builder Canvas
Build complex routing flows visually with a drag-and-drop canvas -- like n8n or Zapier, but for lead routing.
The visual canvas
When you click Edit Flow on any rule, you enter the freeform canvas. Drag steps from the right sidebar onto the canvas to build your routing flow.
Step types
| Step | Color | Purpose |
|---|---|---|
| Trigger | Purple | Defines which object and event fires the route (Lead/Contact/Account on Insert/Update/Both) |
| Match | Blue | Check for duplicate leads, contacts, or accounts before routing |
| Filter → Assign | Indigo + Green | A condition path paired with its assignment action (User, Round Robin team, or Queue) |
| Default Owner | Amber | Catch-all fallback for records that don't match any path |
Canvas controls
- Zoom -- Scroll wheel or use the +/- buttons in the toolbar (25%-200%)
- Pan -- Hold Space and drag, or use the canvas background
- Fit to view -- Click the maximize icon to center all nodes
- Drag steps -- Drag from the Step Registry panel on the right onto the canvas
- Click to configure -- Click any node to open its config sheet with full options
English View
At the top of the route builder, you'll see two tabs: Canvas and English. Click English to switch to the natural language review that shows your routing logic as plain English sentences.
The English view detects common mistakes and displays warnings:
| Warning | What it means |
|---|---|
| Unconfigured assignment | A path has no assignee set -- records would be unroutable |
| Unreachable path | A path after a catch-all will never execute |
| Multiple catch-alls | More than one path has no conditions -- only the first will trigger |
| No default owner | Records that don't match any path will be left unassigned |
| Dry run active | The rule is in test mode -- it logs but doesn't update owners |
Trigger Conditions
Trigger conditions let you define criteria that records must meet before the routing engine is called. This filtering happens inside Salesforce's Apex trigger, so records that don't match never leave your org.
- Open a rule and click on the Trigger step in the canvas
- In the Trigger config sheet, scroll down to Trigger Criteria
- Add conditions using the condition builder -- same UI as path conditions
- Conditions within a group are joined with AND. Multiple groups are joined with OR.
- Save the rule. Conditions are automatically synced to Salesforce as
Route_Criteria__crecords.
Fuzzy & AI Matching
When configuring the Match step, you can enable company name matching with one of three modes:
- Strict -- Exact normalized match only. "Acme Inc" = "acme"
- Fuzzy -- Levenshtein, Soundex, Double Metaphone + abbreviation dictionary (~85 common mappings)
- AI Smart -- LLM-powered semantic matching with 3-layer alias cache (in-memory, Redis, Postgres)
How fuzzy matching works
The fuzzy matcher runs a cascading resolution chain -- it tries the fastest method first and only falls back to more expensive checks if needed:
- Exact normalized -- Strip suffixes (Inc, LLC, Corp, Ltd, GmbH), lowercase, remove punctuation
- Abbreviation dictionary -- ~85 common mappings (IBM → International Business Machines, HP → Hewlett Packard)
- Levenshtein similarity -- Character-level edit distance, threshold >= 0.85
- Soundex match -- 4-character phonetic codes
- Double Metaphone -- Primary + alternate phonetic encodings
AI Smart mode
When AI Smart is selected, the system adds an additional resolution step after fuzzy matching fails:
- Alias cache check -- L1 in-memory (1,000 entries, 1hr TTL) → L2 Redis (24hr TTL) → L3 Postgres (permanent)
- AI provider call -- Sends company name pair to your configured LLM (Claude, OpenAI, Gemini, or custom endpoint)
- Cache result -- Writes the AI's decision to all cache layers to avoid repeated calls
Condition operators
| Operator | Algorithm | Use case |
|---|---|---|
fuzzy_equals |
Levenshtein >= 0.8 | Typo tolerance -- "Microsft" matches "Microsoft" |
sounds_like |
Soundex + Double Metaphone | Phonetic matching -- "Smith" matches "Smyth" |
similar_to AI |
Full resolution chain + AI | Semantic matching -- "JPMorgan" matches "JP Morgan Chase" |
Step 13: Verify It Works
Create a test Lead and watch it get routed in real time.
End-to-end test
- In Salesforce, create a new Lead that matches your rule (e.g. Company = "Enterprise Corp")
- Within 2-3 seconds, the Lead's Owner should update to the assigned user
- In the web dashboard, go to Activity to see the routing event logged
What to check
| Check | Expected Result |
|---|---|
| Lead Owner in Salesforce | Changed to the assigned user within seconds |
| Activity page in web dashboard | Shows the routed record with status "Success" |
| Rule matches | The rule that matched is shown in the activity detail |
Step 14: Activity & Analytics
Monitor every routing decision in real time.
Routing History
The Activity page shows every record the engine has processed. Navigate to Activity in the sidebar. Use filters to narrow by object type, status, rule, team, or specific assignee.
Status types
| Status | Meaning |
|---|---|
| Success | Record matched a rule and the owner was updated in Salesforce |
| Failed | Rule matched but the engine couldn't update the owner (check error in detail view) |
| Unmatched | No routing rule matched this record -- it was not reassigned |
| Retry | A transient error occurred and the engine will retry automatically |
| Merged | Duplicate record was merged with an existing one |
Activity tabs
- Routing History -- The main event log with full details per record
- Assignment Stats -- Aggregated statistics on routing volume, success rates, and assignee distribution
- Failed Routings -- Filtered view showing only failed events for quick debugging
- Audit Log -- System-level log of configuration changes (rule edits, user licensing, team modifications)
Analytics Dashboard
Navigate to Analytics in the sidebar for a dedicated reporting dashboard with four tabs:
| Tab | What it shows |
|---|---|
| Overview | KPI cards (total routed, success rate, avg response time, conversions), volume trend chart, top rules by volume |
| Rules | Per-rule breakdown -- volume, success/fail/unmatched split, average processing time, last triggered |
| Teams | Per-team and per-member metrics -- assignments, capacity, round robin distribution balance |
| Conversions | Lead-to-Opportunity conversion tracking -- conversion rate by rule, team, and time period with funnel visualization |
Step 15: AI Assistant PRO
Chat with an AI that understands your routing data -- get optimization suggestions, debug issues, and query activity.
What it does
The AI Assistant is a conversational interface that has access to your routing rules, activity history, team configuration, and analytics. It can:
- Answer questions about your routing setup -- "Which rule handles APAC leads?"
- Suggest optimizations -- "How can I improve my conversion rate?"
- Debug routing failures -- "Why did this lead go to the wrong team?"
- Generate reports -- "Show me routing volume by team this week"
- Explain rules in plain English -- "Walk me through the Enterprise Inbound rule"
Configure your AI provider
Navigate to Settings → AI to connect your LLM provider. Bring your own API key (BYOK) -- your data stays on your server.
Supported providers:
- Claude (Anthropic) -- Recommended
- OpenAI -- GPT-4o, o1
- Gemini -- Google AI
- Custom -- Any OpenAI-compatible endpoint
Setup steps
- Go to Settings → AI in the sidebar
- Click Connect on your preferred provider card
- Enter your API key and select a model (e.g.
claude-sonnet-4-5-20250514,gpt-4o) - For Custom endpoints: provide a base URL and optional custom headers for auth
- Click Save -- the key is encrypted and stored server-side
- Navigate to AI Assistant in the sidebar and start chatting
How To Videos
Video walkthroughs to help you get the most out of Lead Router.
Getting Started in 5 Minutes
Complete setup walkthrough from VPS to first route
Building Your First Route
Learn the route builder and condition system
AI-Powered Matching
Using fuzzy and AI matching for intelligent routing
Frequently Asked Questions
1 vCPU, 2 GB RAM, and 20 GB disk is the absolute minimum. We recommend 2 vCPU and 4 GB RAM for production workloads. Any Linux distribution with Docker support works (Ubuntu 22+, Debian 12+, CentOS 8+). Root SSH access is required for the initial deployment.
Enterprise, Unlimited, Developer, and Performance editions are fully supported. Sandbox orgs work too -- just use https://test.salesforce.com as the login URL during setup. Professional Edition is not supported because it doesn't allow Apex triggers.
Run npx @lead-routing/cli deploy from the same directory where you ran init. This pulls the latest Docker images and recreates the containers with zero downtime. Database migrations run automatically on startup.
Yes. Lead Router supports Lead, Contact, and Account objects. Enable additional objects in the Integrations page, then create routing rules with the appropriate object type selected. Each object has its own triggers and field schema.
If the routing engine is unreachable, Salesforce's Apex trigger will log the error to Routing_Error_Log__c in your org. Records are not modified -- the existing owner stays unchanged. The engine uses BullMQ for job processing, so any in-progress jobs will be retried when the service comes back online.
AI matching uses a 3-layer alias cache (in-memory, Redis, Postgres) and falls back to your configured LLM provider when a match isn't cached. It's designed for company name matching where traditional fuzzy algorithms fall short -- for example, recognizing that "JPMorgan" and "JP Morgan Chase & Co" are the same entity. You bring your own API key, and results are cached to minimize API calls.
No. Lead Router is fully self-hosted. All data -- routing rules, activity logs, Salesforce credentials, and user data -- stays on your VPS. The only external communication is the license check heartbeat (Pro plan) and optional AI provider API calls that you configure.
Go to License Users in the web dashboard and click "License Users" to sync from Salesforce. You can add users individually, by role, or by profile. Free tier allows 3 licensed users; Pro tier is unlimited.
Troubleshooting
Common issues and how to resolve them.
Health check times out during CLI init
Cause: DNS hasn't propagated yet, or ports 80/443 are blocked.
Fix:
- Verify DNS:
dig app.acme.comshould return your VPS IP - Check firewall:
ufw status(Ubuntu) -- ports 80 and 443 must be open - Check Docker: SSH into VPS and run
docker ps-- all 5 containers should be running - Let's Encrypt rate limit: if you've provisioned too many certificates for the same domain, wait 1 hour
redirect_uri_mismatch error during Salesforce login
Cause: The callback URL in your Connected App doesn't match the one Lead Router is using.
Fix: In Salesforce Setup → App Manager → your Connected App → Edit, verify the Callback URL is exactly:
https://app.acme.com/api/auth/sfdc/callback
Replace app.acme.com with your actual app domain. No trailing slash.
Records are not being routed
Check these in order:
- Triggers active? -- Open "Lead Router Setup" in Salesforce App Launcher and check Step 2 shows triggers are activated
- Rule matches? -- Ensure your rule conditions match the Lead field values. Check the Activity page for "Unmatched" entries.
- Custom Settings configured? -- In Salesforce Setup, search "Custom Settings" → "Routing Settings" → Manage. The Engine Endpoint should be your API URL.
- Engine reachable? -- Run
curl https://api.acme.com/healthfrom any machine. Should return{"status":"ok"}
"Lead Router Setup" doesn't appear in App Launcher
Cause: The LeadRouterAdmin permission set isn't assigned to your user.
Fix: Salesforce Setup → Users → Permission Sets → Lead Router Admin → Manage Assignments → Add your user.
Viewing server logs
SSH into your VPS and use Docker logs:
# Web app logs
docker logs lead-routing-web-1 --tail 100 -f
# Engine logs
docker logs lead-routing-engine-1 --tail 100 -f
# All services
docker compose -f ~/lead-routing/docker-compose.yml logs --tail 50 -f
Redeploying the Salesforce package
If you need to redeploy the Apex triggers and components:
This re-deploys all Apex classes, triggers, and configurations to your Salesforce org. Run this from the same directory where you ran init.
Updating Lead Router to a new version
This pulls the latest images and recreates the containers with zero downtime.
Fuzzy matching not working -- exact matches only
Cause: The match mode is set to "Strict" (default) instead of "Fuzzy" or "AI Smart".
Fix:
- Open the rule in the Route Builder and click the Match step
- In the config sheet, look for Company Match Mode
- Switch from "Strict" to "Fuzzy" or "AI Smart"
- Save the rule -- fuzzy matching takes effect immediately on the next routing event
If using AI Smart mode, also verify your API key is configured in Settings → AI and the provider is reachable from your server.
AI Assistant not responding or returning errors
Check these in order:
- API key valid? -- Go to Settings → AI and verify the key shows "Connected". Try disconnecting and reconnecting.
- Provider reachable? -- SSH into your VPS and test:
curl https://api.anthropic.com/v1/messages(or your provider's API). Firewall rules may block outbound HTTPS. - Rate limited? -- Check the engine logs for 429 responses. The AI assistant respects provider rate limits and will queue retries.
- Custom endpoint? -- Verify the base URL includes the correct path prefix and that any custom headers (auth tokens) are correct.
Trigger criteria not syncing to Salesforce
Cause: The sync from web app to Salesforce Route_Criteria__c records failed silently.
Fix:
- Check Salesforce connection -- Go to Integrations and verify the Salesforce org shows "Connected". Reconnect if the OAuth token expired.
- Verify the object exists -- In Salesforce Setup, search for "Route Criteria" under Custom Objects. If missing, redeploy the package from Integrations → Deploy Package.
- Manual sync -- Open the rule, make any small edit to a trigger condition, and save again. This forces a re-sync.
- Check logs -- Look at web app logs (
docker logs lead-routing-web-1 --tail 50) forSFDC_SYNC_ERRORmessages with details.
Changelog
Feature v0.5.0
License management, weighted team distribution, trigger criteria filtering, AI assistant improvements.
Feature v0.4.0
Analytics dashboard, audit logging, rule cloning, bulk license management.
Improvement v0.3.0
Route builder redesign, English-language rule view, improved onboarding wizard.