Skip to content

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:

  1. Origin default: an Organization Origin can define a default limit profile (and a separate onboarding limit profile), applied to organizations created from that origin.
  2. Voucher: redeeming a voucher at signup assigns the voucher's limit profile, replacing whatever the origin set.
  3. 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:

just run cleanup_expired_organizations --dry-run
just run send_expiration_reminders --dry-run

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.