OwnServa

← Documentation

Phase 5: billing and domains

Update: completion work adds account/team controls, owned DNS, private GitHub, Git automation and scheduled cold backups. Where earlier limits below differ, that guide describes the current implementation and required migrations.

The dashboard now exposes Billing and Domains backed by PostgreSQL, owner/admin authorization, audit records and provider verification. Apply migrations 007, 008 and 009 in order after the Phase 4 migrations. Existing databases need explicit migrations; Docker initialization scripts run only against an empty data directory. Back up the database and encryption key first.

Pricing and administration

The proposed starting prices are USD 9/month Pro (5 servers) and USD 29/month Business (25 servers). Free allows one server when enforcement is enabled. These are platform fees; customer hardware, domain purchases, payment fees, tax and external storage are separate. They are starting commercial assumptions, not a profitability guarantee. For context, Coolify and Railway have different self-hosted/compute pricing models.

Billing includes a price editor visible only to a super_admin. An operator must promote the intended existing user in PostgreSQL; self-promotion has no API. For example, with an operator connection, UPDATE users SET platform_role='super_admin' WHERE id='VERIFIED_USER_UUID';. This grants catalog administration without bypassing tenant membership. The editor changes currency, monthly price in minor units, server allowance, native provider plan IDs and availability. Existing checkouts and subscriptions retain captured prices and allowances. Changing current subscriptions requires cancellation and a new checkout; automatic prorating is not implemented.

Plans start disabled. Create matching monthly, unlimited, fixed-price native plans with the merchant. PayPal trial cycles and setup fees are rejected. Enable a plan only once its provider price validates. Configure API-only credentials using .env.example; never put merchant secrets in browser variables.

  • Paystack: PAYSTACK_SECRET_KEY=sk_test_...; recurring subscription creation follows a verified first charge and requires reusable authorization. The native email token is encrypted.
  • PayPal: PAYPAL_MODE=sandbox, client ID, client secret and webhook ID for the actual sandbox webhook subscription. Use a real sandbox transaction; simulated webhook signatures do not provide the same postback verification.
  • BILLING_RETURN_URL must be the HTTPS control-plane Billing page. Returning to the page does not prove payment: the owner presses Verify, which reads provider state server-side.
  • Configure public POST webhook URLs /webhooks/paystack and /webhooks/paypal on the API ingress. Preserve exact raw bodies and Paystack signature headers; pass PayPal transmission headers.
  • BILLING_LIVE_ENABLED=true is required for live credentials. BILLING_ENFORCE_LIMITS=true enables server-pairing allowances and paid-only custom-domain binding, historical metrics and backup jobs. With live billing enabled, sandbox payments never grant paid entitlement. Keep enforcement disabled while migrating existing workspaces until their billing has been reconciled.

Subscriptions use provider-native recurrence, explicit consent, verified amounts/currencies/identities, deduplicated payment/event records and three-day failed-payment grace. Cancellation stops future recurrence while preserving the verified paid period. Immutable receipts can be printed from Billing; these are payment receipts, not jurisdiction-specific tax invoices. Refunds/reversals associated with PayPal subscription sales put access on operator review hold and cannot be undone by a replayed payment event. Review both subscriptions.review_required and billing_checkouts.review_required before restoring access, reconciling the merchant and the paid period first. Payment refunds, tax handling, disputes without an associated native agreement, and Paystack refund reconciliation require merchant/operator procedures; no automatic refund endpoint exists.

Uncertain checkout/subscription outcomes remain reserved and must be checked with the merchant. Do not reset an unknown row or create another native agreement merely to clear the UI. Hosted approvals may remain payable after a local expiry. An operator must cancel the old provider approval/agreement before closing a pending checkout in PostgreSQL. Receipts and payments must remain immutable. Loss of a response after native creation is intentionally not retried automatically.

Registrar setup

Openprovider requests use its fixed HTTPS API, a scoped OPENPROVIDER_TOKEN, a restricted DOMAIN_TLDS list (default com/net/org), one-year quotes, and integer retail markup (DOMAIN_MARKUP_BPS=2000 means 20%). Premium domains require operator review. Refresh an expiring Openprovider token externally. See the Openprovider documentation and validate API contracts against the operator account before enabling purchases.

First create and verify contacts with Openprovider, including registry-specific eligibility and required additional data. Then use an operator database connection to run scripts/provision-registrant.mjs ORGANIZATION_UUID PRIVATE_PROFILE_JSON com,net. Set the operator DATABASE_URL and the same SECRET_ENCRYPTION_KEY used by the API. The private JSON contains string fields owner_handle, admin_handle, tech_handle, billing_handle and optionally additional_data as a JSON-encoded string. Keep this file outside the repository with private file permissions. The script encrypts it with organization-bound AES-GCM and approves only the listed TLDs. Verification expires after one year. Users cannot submit arbitrary contact handles through checkout.

Registration, renewal and transfer have separate, exact-price hosted payments. Sandbox payments exercise checkout but cannot execute a live registrar operation. Execution requires a verified live domain payment, a fresh quote that has not increased, explicit confirmation and OPENPROVIDER_LIVE_ENABLED=true. Transfers require an authorization code; nameservers require two to six valid hostnames. A billable registrar POST is submitted once. A missing response becomes unknown; inspect the provider before manual reconciliation. A returned request is not registration success: read-back must confirm the exact domain and active registrar state, and renewal must advance expiry. A price increase or failed purchase after collection needs operator refund/requote handling; the app does not silently collect more.

Auto-renew is off initially. Enabling it also requires DOMAIN_AUTORENEW_ENABLED=true and an operator funding/renewal-charge policy; registrar auto-renew can debit the operator balance. It does not create an automatic customer renewal payment. Check expiry and renew manually until that policy exists.

For an external domain, connect it, save the one-time TXT challenge and publish _ownserva-verification.NAME. The API stores only its hash. Verify ownership before binding to an application with a public tunnel. Proof expires after 30 days and can be rotated. Configure the displayed CNAME or apex ALIAS/ANAME with the current DNS provider. Ownserva does not overwrite customer-owned DNS zones. Ownership and public routing have separate states: the worker marks routing active only after expected gateway DNS, trusted TLS and the current release header all verify. TLS issuance also checks proof freshness. Moving an already verified domain between organizations requires operator ownership reconciliation.

Acceptance before live use

Run both merchants' sandbox checkout, cancellation and retry flows; deliver duplicate, forged and out-of-order webhooks; verify mismatched amounts, currency, plan and tenant IDs never grant access. Test provider outages, lost creation responses, receipt uniqueness, grace expiry and refund holds. Use a disposable registrar domain to exercise exact quote, verified registrant, separate live payment, at-most-once registration, pending transfer, renewal expiry advancement, nameservers and expiry reporting. Verify TXT rotation, wrong-owner denial and public HTTPS independently. No live merchant transaction or domain purchase was performed in this workspace.