Service packs
A service pack is a curated bundle: one billable line that includes several (service, plan) pairs. Useful for "everything you need to evaluate ScaiLabs" (a trial bundle), regional add-on combos, or seat-priced enterprise bundles.
Data model#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Pricing#
The pack's headline price is service_packs.base_price_cents. It is not the sum of constituent plan prices — packs typically apply a discount. The two discount_type values:
fixed_price—base_price_centsIS the customer's monthly bill. The constituent plans are documentation, not the source of truth.percentage— multiply the sum of constituent (or override) prices by(100 - discount_percentage) / 100. Use this when the bundle math should be transparent.
Per-item override_price_cents lets you tweak individual lines (e.g. "the ScaiVault plan is half-price in this bundle"). Setting it to null inherits the underlying plan's base_price_cents.
Subscription model#
When a tenant subscribes to a pack:
- A
pack_subscriptionsrow is created. - One
subscriptionsrow is created per pack item, withpack_subscription_idpointing back. - The child subscription rows share the pack's
statusand lifecycle.
When the pack is cancelled, the cancel cascades to every child subscription with pack_subscription_id = <pack_sub.id>.
Why both rows?#
The duplication isn't free, but it serves two real needs:
- Quota enforcement. Per-service quota checks (e.g. ScaiKey's "monthly active users") run against the
subscriptionstable joined toplans. If the only record of a tenant's access were the pack row, every quota check would have to JOIN throughpack_items— slower and less indexable. - Mixed subscriptions. A tenant can have both pack subscriptions AND standalone subscriptions to services NOT in any pack. The single subscriptions table makes this work without branching every query.
Events#
Pack lifecycle has its own event topics, separate from regular subscription events:
| Topic | Trigger |
|---|---|
pack_subscription.activated.v1 |
Pack subscribe succeeds |
pack_subscription.changed.v1 |
(Reserved for future — no live producer yet) |
pack_subscription.cancelled.v1 |
Pack cancellation, immediate or scheduled |
Child subscription rows do NOT independently emit subscription.activated.v1 / cancelled.v1 — subscribers see one event per pack action, with pack_includes[] enumerating the constituent services. This avoids double-counting on the CRM side.
Admin surface#
- List packs:
GET /admin/packs - Create pack:
POST /admin/packswithitems[] - Update pack:
PUT /admin/packs/{id}(replaces items wholesale) - Deactivate pack:
DELETE /admin/packs/{id}(soft —is_active=false) - Pack subscriptions across tenants:
GET /admin/packs/subscriptions
The admin UI lives at /admin/packs. See Admin — service packs for the full request/response shapes.
Trial bundles#
The most common pattern: a trial-bundle pack that includes the trial plan of every service. €0 / 14-day trial / public. Tenants subscribing to it land on every service simultaneously at zero cost; when the 14 days lapse, the reaper flips each child subscription to cancelled (unless they upgraded individual services in the meantime).