Billing and Pricing
This page explains how billing works in Servala - from setting up pricing in the catalog to generating invoice line items when customers create service instances.
Overview
Billing in Servala connects four systems:
-
Servala Website - the source of truth for all pricing data and catalog definitions
-
Odoo - the ERP system for invoices, partner records, and product variants
-
Servala Portal - where the pricing catalog is cached and billing line items are calculated
-
Kubernetes - where billing annotations are attached to service instance resources for downstream processing
The general flow is:
-
The website defines all pricing data (SLA tiers, compute plans, storage pricing, discount brackets)
-
The website syncs product variants into Odoo (with prices)
-
The portal syncs the pricing catalog from the website via HTTP API
-
When a customer creates or updates a service instance, the portal calculates billing line items
-
These line items (product IDs and quantities) are written as annotations on the Kubernetes resource
-
An external billing controller reads the annotations and sends billing events to Odoo
-
Odoo processes these billing events and creates invoices accordingly
Billing Catalog (synced from website)
The billing catalog contains all pricing-related data. These models are read-only in the portal - they are synced from the Servala Website and marked as "stale" if not recently updated.
SLA Tiers
SLA tiers define the service level options available to customers.
Each tier specifies:
-
Code - unique identifier (e.g.
nosla,sla999247) -
Uptime percentage - the guaranteed uptime (e.g. 99.9%)
-
HA replica minimum - how many replicas are required (affects compute cost)
-
Service level value - the value written to the Kubernetes CRD (e.g. "besteffort", "guaranteed")
Configured in: Django Admin > SLA Tiers (read-only, synced from website)
Service Pricing
Base pricing for a service (without any SLA) is defined per service:
-
Base fee (no SLA) - a flat monthly fee charged for every instance
-
Unit rate (no SLA) - a per-GiB-RAM monthly rate
-
Currency - currently CHF
Configured in: Django Admin > Service Pricing Configs (read-only, synced from website)
SLA Surcharges
On top of the base pricing, each SLA tier can add surcharges per service:
-
Base fee surcharge - additional flat monthly fee for choosing this SLA tier
-
Per-unit surcharge - additional per-GiB-RAM rate
-
Minimum units - minimum RAM required for this SLA tier (e.g. Premium may require at least 4 GiB)
Configured in: Django Admin > Service SLA Surcharges (read-only, synced from website)
Compute Plan Catalog
Defines the available compute sizes per service and cloud provider:
-
Plan name - e.g. "standard-1", "cpu-4", "large"
-
RAM (GiB) - a positive integer amount of RAM
-
vCPUs - the CPU allocation
-
Monthly price - fixed monthly cost for this size
-
Odoo product ID - links to the Odoo product for invoicing
Configured in: Django Admin > Compute Plan Catalog Entries (read-only, synced from website)
Storage Catalog
Defines storage pricing per cloud provider:
-
Storage class - e.g. "standard"
-
Price per GiB - monthly rate per GiB of storage
-
Odoo product ID - links to the Odoo product for invoicing
Configured in: Django Admin > Storage Catalog Entries (read-only, synced from website)
Progressive Discount Brackets
Volume discounts on the managed service sizing fee, based on RAM allocation. Larger instances get progressively cheaper rates.
Each bracket specifies a RAM range (e.g. 0-3 GiB, 3-8 GiB, 8-16 GiB, 16+ GiB) and a discount percentage. When calculating pricing, the instance’s compute plan RAM is split across brackets and each portion is discounted accordingly. These brackets are global - shared across all services.
Configured in: Django Admin > Progressive Discount Brackets (read-only, synced from website)
Service Component Specs
Defines how the portal extracts billing-relevant values from a service instance’s Kubernetes spec. This tells the billing calculator where to find:
-
Replica count - how many replicas the instance runs (path into the spec, e.g.
spec.parameters.instances) -
Storage amounts - which storage fields to bill for, with their labels and storage classes
For composite services (e.g. Nextcloud which includes PostgreSQL), each component is defined separately with a role of "main" or "supporting".
Configured in: Django Admin > Service Component Specs (read-only, synced from website)
Additional CSP Resources
Defines extra cloud provider resources that are billed alongside a service instance, such as IP addresses, egress, or load balancers.
Each entry specifies:
-
Service and provider - which service/provider combination this applies to
-
SKU and label - identification and display name
-
Quantity - how many units to bill
-
Monthly price - cost per unit
-
Product ID - the Odoo product ID for invoicing
Configured in: Django Admin > Additional CSP Resources (read-only, synced from website)
Compute Plans (portal-managed)
Unlike the billing catalog above, compute plans are managed directly in the portal.
Compute Plans
A compute plan defines a CPU and memory resource allocation:
-
Name - e.g. "standard-1", "cpu-4", "large"
-
CPU requests/limits - Kubernetes CPU values
-
Memory requests/limits - Kubernetes memory values
-
Active - whether this plan is available for selection
Configured in: Django Admin > Compute Plans
Compute Plan Assignments
A compute plan assignment links a compute plan to a specific service on a specific control plane (technically to a ControlPlaneCRD). This controls:
-
Which plans appear for a given service on a given infrastructure
-
Sort order - the display order in the customer-facing UI
-
Active - can be deactivated without removing
-
Proposed storage - the default storage size shown in the UI for this plan
The sync_plan_catalog command auto-manages these assignments: it creates new ones for valid catalog pairs and deactivates ones that no longer have matching catalog entries.
Configured in: Django Admin > Compute Plans > (select plan) > Assignments inline
Billing Entities and Organizations
Billing Entities
A billing entity represents the legal entity that receives invoices. It links to two Odoo partner records:
-
Company partner (
odoo_company_id) - the company in Odoo -
Invoice partner (
odoo_invoice_id) - the invoice address in Odoo
Multiple organizations can share the same billing entity.
Configured in: Django Admin > Billing Entities
Organization Origin (billing defaults)
Each organization is created through an "origin" (e.g. "Servala Direct", "Exoscale Marketplace"). The origin defines billing defaults:
-
Default billing entity - automatically assigned to new organizations from this origin
-
Default sale order ID - a pre-assigned Odoo sale order for all organizations from this origin
-
Invoice grouping - whether invoice lines are grouped "by service" or "by organization"
-
Hide billing address - hides billing address in the UI (useful when billing is handled externally)
-
Billing message - custom text shown instead of the billing address
Configured in: Django Admin > Organization Origins > Billing section
Organization billing setup
When an organization is created:
-
If the origin has a default sale order ID, the organization uses that
-
Otherwise, a new Odoo sale order is created automatically, linked to the billing entity’s company and invoice partners
-
The sale order ID is stored on the organization for all future billing
Configured in: Django Admin > Organizations (billing_entity and sale order fields)
How a Service Instance Gets Billed
When a customer creates or updates a service instance, the portal calculates the billing line items and writes them as Kubernetes annotations.
The calculation
The portal builds five types of line items:
-
Compute - for each component (main service + any supporting services):
replicas x monthly plan price -
Storage - for each storage field per component:
GiB x component replicas -
CSP resources - additional cloud provider resources (IPs, egress, load balancers): fixed quantity per resource
-
Managed service fee - a flat fee:
base fee + SLA surcharge -
Managed sizing fee - per progressive discount bracket:
units in bracket x main replicas, with each bracket’s rate calculated from the base unit rate plus SLA surcharge minus bracket discount
Product ID format
Each line item gets a product ID that maps to an Odoo product:
-
Compute: uses the
odoo_product_idfrom the matchingComputePlanCatalogEntry -
Storage: uses the
odoo_product_idfrom the matchingStorageCatalogEntry -
CSP resources: uses the
product_idfrom theAdditionalCSPResource -
Managed service:
servala-{service}-{sla}-managed -
Managed sizing:
servala-{service}-{sla}-managed-b{bracket}
Kubernetes annotations
The calculated line items are written to the Kubernetes resource as annotations:
-
billing.servala.com/items- JSON array with all line items, each containingproductID,value(quantity),itemDescription, anditemGroupDescription -
billing.servala.com/salesOrderID- the organization’s Odoo sale order reference
The portal writes quantities and product IDs only - actual prices are resolved by Odoo from its product variant records.
Additionally, the organization’s Kubernetes namespace carries billing labels and annotations:
Labels:
-
billing.servala.com/companyPartnerID- Odoo company partner ID -
billing.servala.com/invoicePartnerID- Odoo invoice partner ID
Annotations:
-
billing.servala.com/billingEntity- billing entity name -
billing.servala.com/salesOrderID- sale order reference
Configuration Checklist
To set up billing for a new service, the following must be in place:
-
SLA tiers are defined in the website and synced to the portal
-
Service pricing config exists for the service in the website and synced
-
SLA surcharges are defined for each SLA tier on the service in the website and synced
-
Compute plan catalog entries exist for each plan/provider combination in the website and synced
-
Storage catalog entries exist for each provider in the website and synced
-
Service component specs define how to extract billing data from the spec (in the website and synced)
-
Additional CSP resources are defined if the service needs them (in the website and synced)
-
sync_plan_cataloghas been run successfully -
ServiceDefinition.billing_configis configured for each service (portal-side) -
Compute plans are created in the portal with correct resource values
-
Compute plan assignments link plans to the correct ControlPlaneCRDs
-
Organization origin has correct billing defaults (billing entity, invoice grouping)
-
Billing entity exists with valid Odoo company and invoice partner IDs
-
Odoo product variants exist for all product IDs the portal will emit (synced by the website)