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:

  1. Servala Website - the source of truth for all pricing data and catalog definitions

  2. Odoo - the ERP system for invoices, partner records, and product variants

  3. Servala Portal - where the pricing catalog is cached and billing line items are calculated

  4. Kubernetes - where billing annotations are attached to service instance resources for downstream processing

The general flow is:

  1. The website defines all pricing data (SLA tiers, compute plans, storage pricing, discount brackets)

  2. The website syncs product variants into Odoo (with prices)

  3. The portal syncs the pricing catalog from the website via HTTP API

  4. When a customer creates or updates a service instance, the portal calculates billing line items

  5. These line items (product IDs and quantities) are written as annotations on the Kubernetes resource

  6. An external billing controller reads the annotations and sends billing events to Odoo

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

  1. If the origin has a default sale order ID, the organization uses that

  2. Otherwise, a new Odoo sale order is created automatically, linked to the billing entity’s company and invoice partners

  3. 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:

  1. Compute - for each component (main service + any supporting services): replicas x monthly plan price

  2. Storage - for each storage field per component: GiB x component replicas

  3. CSP resources - additional cloud provider resources (IPs, egress, load balancers): fixed quantity per resource

  4. Managed service fee - a flat fee: base fee + SLA surcharge

  5. 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_id from the matching ComputePlanCatalogEntry

  • Storage: uses the odoo_product_id from the matching StorageCatalogEntry

  • CSP resources: uses the product_id from the AdditionalCSPResource

  • 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 containing productID, value (quantity), itemDescription, and itemGroupDescription

  • 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:

  1. SLA tiers are defined in the website and synced to the portal

  2. Service pricing config exists for the service in the website and synced

  3. SLA surcharges are defined for each SLA tier on the service in the website and synced

  4. Compute plan catalog entries exist for each plan/provider combination in the website and synced

  5. Storage catalog entries exist for each provider in the website and synced

  6. Service component specs define how to extract billing data from the spec (in the website and synced)

  7. Additional CSP resources are defined if the service needs them (in the website and synced)

  8. sync_plan_catalog has been run successfully

  9. ServiceDefinition.billing_config is configured for each service (portal-side)

  10. Compute plans are created in the portal with correct resource values

  11. Compute plan assignments link plans to the correct ControlPlaneCRDs

  12. Organization origin has correct billing defaults (billing entity, invoice grouping)

  13. Billing entity exists with valid Odoo company and invoice partner IDs

  14. Odoo product variants exist for all product IDs the portal will emit (synced by the website)