Skip to content

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.

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_budget

The Free subscription uses the same invoice.paidreset_cycle_budget_in_txn renewal path as paid tiers. No code bifurcation — the billing engine treats it identically.

Migration m20260423_120000_seed_free_subscription_tier inserts a single row into subscription_tier:

ColumnValue
slugfree
nameFree
stripe_product_id'' (empty — operator fills post-deploy)
sort_order0
All *_included columns0 (allowed but no baseline allowance)
Feature flagsFALSE
call_analysis_levelbasic

Both signup paths call create_free_subscription after session creation:

  • Email/passwordsignup_api.rs calls it at the end of _handler
  • OAuthcreate_organization_with_twilio_service.rs calls 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 creation
if 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"
);
}

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 from subscription_tier
  • AppError::Internalstripe_product_id is 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.

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 status

The 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.

TierCard in billing_section_component.rs handles three price states based on the Stripe-sourced prices array:

StateDisplay
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.

  1. Run migrations (just generate)
  2. Create a Stripe test-mode Product with a $0 recurring 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"
  3. Paste the prod_XXX Product ID into the Free tier’s Stripe Product ID field at /admin/tab/billing
  4. Configure allowances on the Free tier row (credits, included amounts)
FileRole
migration/.../m20260423_120000_seed_free_subscription_tier.rsSeeds the Free tier row
services/create_free_subscription_service.rsCreates Stripe $0 Subscription
services/dispatch_stripe_event_service.rsCancellation auto-downgrade logic
api/signup_api.rsEmail/password signup integration
services/create_organization_with_twilio_service.rsOAuth signup integration
components/billing_section_component.rsTier card price display
types/subscription_tier_info_type.rsis_checkout_ready field docs