Free Tier
Every organization in Loquent is always on a plan. The Free tier is a permanent $0 Stripe recurring subscription that gets auto-assigned at signup and serves as the downgrade target when a paid subscription is canceled.
How It Works
Section titled “How It Works”Signup (email or OAuth) → Create Stripe Customer → Create session → create_free_subscription(db, stripe_customer_id) → Stripe fires customer.subscription.created webhook → upsert_subscription materializes organization_subscription + org_budgetThe Free subscription uses the same invoice.paid → reset_cycle_budget_in_txn renewal path as paid tiers. No code bifurcation — the billing engine treats it identically.
Database Seed
Section titled “Database Seed”Migration m20260423_120000_seed_free_subscription_tier inserts a single row into subscription_tier:
| Column | Value |
|---|---|
slug | free |
name | Free |
stripe_product_id | '' (empty — operator fills post-deploy) |
sort_order | 0 |
All *_included columns | 0 (allowed but no baseline allowance) |
| Feature flags | FALSE |
call_analysis_level | basic |
Auto-Subscribe at Signup
Section titled “Auto-Subscribe at Signup”Both signup paths call create_free_subscription after session creation:
- Email/password —
signup_api.rscalls it at the end of_handler - OAuth —
create_organization_with_twilio_service.rscalls it after messaging setup
The call is non-blocking: if Stripe is down, signup succeeds and the error is logged. The org lands without an active subscription row and needs manual operator retry.
// signup_api.rs — after session creationif let Err(e) = create_free_subscription(&db, &stripe_customer_id).await { tracing::error!( error = %e, org_id = %organization_id, stripe_customer_id = %stripe_customer_id, "Failed to create Free subscription at signup" );}create_free_subscription Service
Section titled “create_free_subscription Service”File: src/mods/billing/services/create_free_subscription_service.rs
Reads the Free tier’s stripe_product_id from the database, fetches its active monthly Price from Stripe, then creates a Stripe Subscription on the given Customer using that Price ID. Returns the Stripe Subscription ID.
pub async fn create_free_subscription( db: &DatabaseConnection, stripe_customer_id: &str,) -> Result<String, AppError>Errors:
AppError::NotFound— Free tier row missing fromsubscription_tierAppError::Internal—stripe_product_idis empty (operator hasn’t wired it), no active monthly Price found on the Product, or the Stripe API call fails
Stripe waives payment method requirements for $0 Prices, so no default_payment_method is needed.
Auto-Downgrade on Cancellation
Section titled “Auto-Downgrade on Cancellation”When a paid subscription is canceled, mark_subscription_canceled in dispatch_stripe_event_service.rs auto-creates a Free subscription on the same Customer:
customer.subscription.deleted webhook → Filter by stripe_subscription_id (race-safe) → Mark row as canceled → If tier was paid → create_free_subscription(db, stripe_customer_id) → Stripe fires customer.subscription.created for the new Free sub → upsert_subscription overwrites row to Free tier, active statusThe stripe_subscription_id filter prevents race conditions: if a rapid cancel-and-resubscribe already superseded this subscription in the DB, the handler skips instead of overwriting the newer row.
If the auto-downgrade call fails, the error is logged but the webhook returns success. Stripe does not retry deletion events, so the org is left in a canceled state for manual intervention.
UI Changes
Section titled “UI Changes”TierCard in billing_section_component.rs handles three price states based on the Stripe-sourced prices array:
| State | Display |
|---|---|
No active Prices (prices.is_empty()) | “Contact sales” with disabled CTAs |
Wired, $0 | ”$0” (no decimals or suffix) |
| Wired, paid | ”$X.XX/seat/mo” (from the monthly TierPrice) |
The check uses !tier.is_checkout_ready (derived from !prices.is_empty()) to distinguish unwired tiers from the intentionally free tier.
Operator Setup
Section titled “Operator Setup”- Run migrations (
just generate) - Create a Stripe test-mode Product with a
$0recurring monthly Price:Terminal window stripe products create --name="Loquent Free"stripe prices create \--currency=usd --unit-amount=0 \--product=prod_XXX \-d "recurring[interval]=month" - Paste the
prod_XXXProduct ID into the Free tier’s Stripe Product ID field at/admin/tab/billing - Configure allowances on the Free tier row (credits, included amounts)
Key Files
Section titled “Key Files”| File | Role |
|---|---|
migration/.../m20260423_120000_seed_free_subscription_tier.rs | Seeds the Free tier row |
services/create_free_subscription_service.rs | Creates Stripe $0 Subscription |
services/dispatch_stripe_event_service.rs | Cancellation auto-downgrade logic |
api/signup_api.rs | Email/password signup integration |
services/create_organization_with_twilio_service.rs | OAuth signup integration |
components/billing_section_component.rs | Tier card price display |
types/subscription_tier_info_type.rs | is_checkout_ready field docs |