Trials and Expiration¶
Servala organizations can be given a limited lifetime. This powers trial offerings: an organization is created, stays fully usable for a validity period, then expires and is eventually torn down automatically. This page explains how the trial lifecycle works and how to configure it.
Trials are driven entirely by limit profiles and the automated cleanup task. There is no separate "trial" object: any organization whose limit profile (or per-organization override) sets a validity period behaves as a trial.
The trial lifecycle¶
An organization moves through up to three states:
- Active
- From creation until the expiration date. The organization is fully usable within the bounds of its limit profile.
- Expired (grace period)
- From the expiration date until the deletion date. Creating or modifying resources is disabled and the running service instances are paused (scaled down). Existing data is retained. The organization can still be extended by staff during this window.
- Deleted (decommissioned)
- At the deletion date. All service instances and their Kubernetes namespaces are deleted and the organization is archived. This is permanent.
If no grace period is configured, the organization stays in the expired (read-only) state indefinitely and is never deleted automatically.
How the dates are calculated¶
The expiration date is created_at + validity_days. The deletion date is
expiration_date + grace_period_days.
Both dates are then rounded forward to the next business action moment: the next weekday at the configured action hour (default 10:00 Europe/Zurich). Rounding is forward-only, so a trial is never shortened, and expiry or teardown never lands at night or on a weekend. A trial that would technically expire on a Saturday evening expires the following Monday morning instead.
Configuring trials¶
Limit profiles¶
Trials are configured on the Organization Limit Profile model in Django Admin. A limit profile is a reusable bundle of constraints that can be attached to many organizations. The two fields that make a profile a trial are:
- Validity period (days)
- Days from organization creation until expiration. Leave empty for no expiration (the organization never becomes a trial).
- Grace period (days)
- Days after expiration before the organization and its resources are automatically deleted. Leave empty to disable automated deletion, so the organization stays expired and read-only but is never removed.
A limit profile can also cap what the trial may consume, independently of the validity period:
- Maximum service instances: the number of instances that can exist. Leave empty for unlimited.
- Allowed service offerings: restrict which offerings may be provisioned. Leave empty to allow all.
- Allowed compute plans: restrict which compute plans may be selected. Leave empty to allow all.
Assigning a limit profile¶
A limit profile reaches an organization in one of three ways:
- Origin default: an Organization Origin can define a default limit profile (and a separate onboarding limit profile), applied to organizations created from that origin.
- Voucher: redeeming a voucher at signup assigns the voucher's limit profile, replacing whatever the origin set.
- Manual: staff assign or change the limit profile on an individual organization in the staff area or Django Admin.
Per-organization override¶
To give a single organization a different validity period without creating a new limit profile, set Validity period override (days) on the organization. It overrides the limit profile's validity period for that organization only. The grace period still comes from the limit profile.
Billing safety of overrides
Shortening or removing a validity period does not refund an already-invoiced trial, and an existing trial window is never removed from Odoo. Extending a trial only ever pushes the free window forward. See Odoo trial window below.
Extending or ending a trial early¶
- Extend: raise the validity period on the limit profile, or set a longer validity override on the organization. The new expiration and deletion dates are recalculated from creation. This is the action taken when a customer asks to keep their trial (they are pointed at the support form by the in-portal expiration banner).
- End early: staff can decommission an organization directly from the staff area rather than waiting for the grace period to elapse. See Organization decommission.
Automated enforcement¶
Two background tasks drive the lifecycle. Both run on the Procrastinate worker and are safe to re-run.
Expiration reminders¶
send_expiration_reminders runs daily and emails all members of each trial
organization at fixed milestones:
| Reminder | When | Content |
|---|---|---|
| 7 days | 7 days before expiration | Warns that resources will be blocked and instances paused |
| 3 days | 3 days before expiration | Same warning, closer to the date |
| Expiration | On the expiration date | Resources are now blocked; if a grace period exists, states the deletion date |
| Final notice | 1 day before deletion | Last chance to keep the organization before deletion |
Each reminder is sent once per organization and tracked in the organization's
expiration_reminders_sent record, so re-runs do not resend. If sending fails
for every recipient, the marker is rolled back so the next run retries.
Cleanup (pause and delete)¶
cleanup_expired_organizations runs hourly (so actions land close to the
configured action hour rather than up to a day late) and, for each trial
organization:
- Pauses the instances of organizations that are expired but still within the grace period.
- Decommissions organizations whose grace period has elapsed: deletes instances and namespaces, archives the organization, and emails the members a deletion confirmation.
Every operation is idempotent, so a partially completed teardown is simply finished on the next run. If an organization fails to tear down, the task records the failure and retries; one stuck organization does not block the others.
You can preview what the task would do without making changes:
Resource blocking on expiration¶
Once an organization is expired, the portal blocks all state-changing requests (create, update, delete) for it, independent of the cleanup task. Users see a banner explaining that the organization is expired and pointing them at the support form to request an extension.
Odoo trial window¶
Trial days must not be billed. When an organization's own Odoo sale order is
created, the portal mirrors the trial window onto it as trial_start_date and
trial_end_date (creation to deletion date). This window is forward-only: it is
extended when a trial is lengthened but never removed, because clearing it would
re-bill historical trial days.
During teardown the portal first pushes trial_end_date forward by a small
buffer (SERVALA_TRIAL_TEARDOWN_BUFFER_DAYS, default 1 day, plus a safety
margin) so that any analytic lines emitted while instances are being deleted
still fall inside the free window and can never become billable.
Organizations on a shared sale order (configured on the origin) do not receive a per-organization trial window.
Configuration reference¶
Cron schedules for the tasks are documented in Background Tasks. The trial timing itself is controlled by these environment variables:
| Variable | Default | Purpose |
|---|---|---|
SERVALA_TRIAL_ACTION_HOUR |
10 |
Hour of day (in the action timezone) at which expiry and teardown land. |
SERVALA_TRIAL_ACTION_TIMEZONE |
Europe/Zurich |
Timezone for the action hour and business-day rounding. |
SERVALA_TRIAL_TEARDOWN_BUFFER_DAYS |
1 |
Days the Odoo trial window is pushed forward during teardown. |