Machine-translated draft — terminology + flow still being reviewed.

Mortar pricing — Cloud credit + 5 capacity tiers

Status: draft, awaiting numerical sign-off. Engineering can start on internal/plan/, real-impl feature/usage Recorder, and the credit ledger once the dimensions here are locked.

See also: Fabric subscription pricing (the other axis) at fabric/docs/pricing-strategy.md. The two-layer billing model is explained from both sides — that doc’s “second-layer cloud bill” section compares Mortar’s ¥99/mo credit positioning against a leading full-stack app cloud / Bolt+Supabase / v0+Vercel etc.


TL;DR

Mortar Cloud is paired with Fabric, and follows the built-in full-stack cloud model:

  • Two independent axes. Fabric subscription (Free / Pro / Max / Ultra — see fabric/docs/pricing-strategy.md) is one axis; Mortar Cloud capacity is the other.
  • 5 Cloud capacity tiers — Tiny / Mini / Small / Medium / Large. Each tier sets a capacity ceiling (DB RAM / disk / connections). No flat per-tier subscription fee.
  • ¥99/mo Cloud credit per workspace — flat, regardless of editor tier or Cloud capacity tier. Resets on the 1st of each month; doesn’t roll over.
  • All Cloud usage metered against credit: instance baseline (per hour the tier is active), variable usage (storage / egress / compute / cache) — all itemized on the dashboard.
  • 3 modes when credit runs out: Hard budget (default — pause writes until next month or manual top-up), Manual top-up (user adds funds), Auto top-up (card charged with monthly cap).
  • Free editor users → forced Tiny tier (the free-tier size gate). Pro / Max / Ultra users → free pick of all 5 tiers.

Capacity tiers

Each tier sets the per-project capacity ceiling. Picking a tier is not a flat-fee subscription — it controls (a) the maximum your DB instance can reach, (b) how fast your credit drains on baseline.

TierDB RAMDB diskDB connStorage capEgress capCache RAM capCompute cap (CPU-min)Hourly baselineMonthly baseline (full month on)
Tiny1 GB10 GB6010 GB25 GB64 MB200¥0.026¥19
Mini2 GB50 GB9050 GB100 GB256 MB1 000¥0.068¥49
Small4 GB100 GB120100 GB250 GB1 GB5 000¥0.207¥149
Medium8 GB200 GB160250 GB500 GB4 GB20 000¥0.693¥499
Large16 GB500 GB240500 GB1 TB4 GB30 000¥1.110¥799

Capacity caps are hard limits — exceeding them blocks new writes or rejects new resources (see Enforcement).

Baseline is the cost of keeping the tier’s DB instance slice provisioned on the shared RDS pool, billed hourly against credit. A project paused for the full month pays no baseline.

Cloud credit

Every workspace receives ¥99/mo Cloud credit, automatically, on the 1st of each calendar month. It doesn’t roll over.

The credit covers all metered usage on Mortar Cloud:

  • Instance baseline (per the tier table above)
  • Variable usage (per the table below)

There is no separate per-tier subscription fee — the ¥99 credit is the only “subscription” for the Cloud half. Heavy users either upgrade their editor plan (which doesn’t change the Cloud credit; Appunvs’s pricing is orthogonal) or use one of the three top-up modes below.

Variable per-unit prices

When usage exceeds the credit, the same per-unit prices continue to apply (now drawn from top-up funds instead of monthly credit):

ResourceUnit priceNotes
Storage¥0.15 / GB-monthOSS-equivalent, billed monthly
Network egress¥0.40 / GBPublic outbound only; ingress free
Compute (CPU-min)¥0.05 / CPU-minAliyun FC equivalent units
Email (per message)¥0.001 / messageAliyun DirectMail equivalent (placeholder; aliyun email adapter TODO)
SMS (per message)¥0.05 / messageAliyun 短信 equivalent (~¥0.045 + thin margin); requires pre-registered SignName + TemplateCode (模板报备)
Cache RAM (overage)¥3 / GB-monthAbove the tier’s bundled cap
DB instance baselineper tier aboveAlways-on; pause the project to stop

These prices are the same across every tier — what differs by tier is the capacity ceiling and the baseline rate.

Three credit modes

The workspace owner picks one of these three, defaulting to the first. Switchable any time from the dashboard.

1. Hard budget (default)

Credit hits ¥0 → project enters read-only mode (writes return 402 credit_exhausted; reads OK). Resumes automatically on the 1st of next month when fresh ¥99 credit is granted.

User can also manually top up at any time to resume early.

2. Manual top-up

User explicitly adds funds (¥99 / ¥299 / custom) to the workspace’s Cloud balance from the dashboard. Credit deducts as usual; the project pauses only when both monthly credit + top-up balance are zero.

3. Auto top-up

User configures: “when balance < ¥10, auto-charge card ¥X, with monthly cap ¥Y”. Card is charged automatically. Project pauses only when the monthly cap is reached.

The monthly cap is the anti-runaway-bill safety: a standard explicit-cap design. Without it, a misbehaving cron job in October could rack up ¥10 000 by the 5th — the cap stops that at any predictable level the user chose.


Why not a flat per-tier subscription?

Three options were considered:

ModelExampleMargin profileUX
A: Flat per-tier (drafted in v3)Tiny ¥99/mo, Mini ¥249/mo, …Lock-in revenue, predictableAccount opaque (“what did I get for ¥99?”)
B: Pure metered (Supabase default)Subscription + per-unit overageHeavy users pay moreBills unpredictable; “spend cap” needed
C: Credit + capacity tiers (this doc)¥99/mo credit, pick tier for capacitySimpler bills than B, transparent vs A”I have ¥99 to spend, here’s how I’m spending it”

We picked C because:

  • Aligned with the earlier pivot decision — users pay for what they actually consume, not a tier-bundled fixed fee
  • Naturally matches our hosted architecture (one shared RDS pool — baseline cost per tenant is hourly anyway)
  • Lower marketing friction in mainland China than B (Spend Cap is a weird concept; “credit” / “balance” is intuitive)
  • Open-source self-hosters on RDS see the same itemized breakdown in their own dashboard — useful regardless of who’s paying

What we lose vs A: the “feels like a subscription” framing. Mitigated by always displaying the ¥99/mo monthly grant, which is psychologically the same as “I’m subscribed”.


Enforcement

Two distinct overflow paths, two different responses.

Capacity overflow — block writes, hint upgrade

Capacity caps (DB RAM / disk / conn / storage / egress / cache / compute) are hard limits. Exceeding any one returns 507 with a structured upgrade hint:

ResourceAt 80%At 95%At 100%
DB diskemailbanner+emailwrites return 507 tier_capacity_exhausted {primitive:"database_disk"}
DB connbannerreject 503(already at 100% — Postgres rejects new conns, Mortar wraps as 503 database_busy)
Storageemailbanner+emailuploads return 507
Network egressemailbanner+emaildownloads continue but flag overflow events
Cache RAMbannerbanner+emailLRU evict (Redis-native)
Computeemailbanner+emailfunction invocations return 503 tier_capacity_exhausted

Reads are NEVER blocked. Otherwise users can’t see their data to clean it down.

DB RAM and CPU saturation are continuous performance dimensions, not discrete events — handled by the noisy-neighbor monitor (see below) in our shared-pool architecture.

Credit overflow — mode-dependent

Per the chosen credit mode (Hard budget / Manual / Auto top-up). In all modes, the user gets clear in-app and email warnings well before credit is exhausted.

Noisy-neighbor (shared-pool only)

Mortar’s hosted Cloud runs all small/medium tenants on one shared RDS instance for COGS reasons. This means a single tenant’s runaway query can degrade other tenants — a concern Supabase avoids by giving each project its own VM.

Active mitigation:

  1. Enable pg_stat_statements; per-query records carry the tenant_id via current_setting('mortar.tenant_id').
  2. Background goroutine: every 60 s, compute per-tenant CPU% and slow-query count over the last 5-min sliding window.
  3. Escalating actions:
LevelTriggerAction
L1tenant CPU% > 15% for 5 minDashboard banner + email
L2tenant CPU% > 30% for 5 minTenant’s PgBouncer pool size temporarily ÷2
L3single query running > 30 spg_terminate_backend(pid) on the query
L4L3 fired > 3 times in 1 hourForce tier upgrade prompt OR project pause 24 h

This entire layer is off by default for self-host deployments — single-tenant deploys don’t have noisy neighbors, and the monitor just consumes CPU. Set MORTAR_NOISY_NEIGHBOR_MONITOR=on for our hosted cloud only.

Self-initiated downgrade past current usage

User on Small (currently using 23 GB DB disk) tries to switch tier to Tiny (10 GB cap). API/dashboard refuses with a structured error:

{
  "error": "tier_change_would_overflow",
  "blockers": [
    {"resource": "database_disk", "current": 24696061952, "new_limit": 10737418240}
  ]
}

(current / new_limit are int64 byte counts, not human-readable strings.)

User must clean up first.

Payment failure (auto top-up mode)

Card declines / wire doesn’t clear:

  • Day 0–3: full service, retry payment daily, alert user
  • Day 4–7: full service, escalating alerts
  • Day 8: project enters read-only mode (writes 402 credit_exhausted; reads OK)
  • Day 30: project paused (no reads, no writes; data preserved)
  • Day 90: project + data deleted (final notice at day 60)

Tier upgrade / downgrade transitions

Tier change: takes effect immediately. No DB downtime — tier is a software cap on the shared pool, switching is a config flip. The new baseline rate begins applying from the moment of change.

Mid-month switching: baseline is metered hourly, so changes prorate naturally. Switch from Tiny (¥0.026/hr) to Small (¥0.207/hr) on day 15 of a 30-day month → days 1–15 charged at ¥0.026/hr, days 16–30 at ¥0.207/hr, all drawn from the same monthly credit balance.

No “credit refund” for downgrade: monthly credit is already granted at month start; using a smaller tier just means slower burn.


COGS analysis (Aliyun RDS-only)

Numbers at expected 50% utilization. Worst case (all dimensions maxed) margin drops ~10 points but is structurally bounded.

Shared-pool assumptions (mainland production deploy):

  • 1 Aliyun RDS PG HA 4-core / 16 GB / 200 GB (~¥1500/mo) hosts ~150 Tiny + ~50 Mini + ~20 Small projects via RLS
  • 1 Tair in-memory 16 GB instance (~¥1600/mo) shared across ~500 small-tier cache allocations
  • OSS / network egress are linear pass-through with markup
  • Aliyun FC: ~¥0.005/CPU-min retail, ~10× markup on CU resource pack
TierDB COGSStorageEgressCacheComputeTotal COGS @50% utilEffective price (baseline + ~50% credit + ~50% overage)Margin
Tiny¥10¥0.5¥4¥3.5¥0.5¥18.5¥85 (within credit, baseline+light variable)78%
Mini¥18¥2.5¥17¥13¥3¥53.5¥285 (some overage)81%
Small¥45¥5¥42¥50¥14¥156¥780 (significant overage)80%
Medium¥165¥12¥85¥200¥55¥517¥2 60080%
Large¥385¥25¥170¥200¥85¥865¥4 30080%

RDS-managed premium absorbed: figures already include the ~30% RDS markup over self-managed Postgres on ECS. We pay the markup; the value is removing ops burden (no DBA needed at current scale).

¥99 monthly credit math:

  • Tiny baseline ¥19/mo alone is ~19% of credit — light user lives comfortably inside the credit
  • Large baseline ¥799/mo exceeds credit alone — user pays ¥700+ baseline overage even at zero variable use; matches the common pattern where Large users effectively buy capacity

Architecture note

Mortar’s hosted Cloud runs entirely on managed cloud services:

PrimitiveHosted backendSelf-host backend
DatabaseAliyun RDS PostgreSQL HAAny Postgres 16+ (RDS / Aurora / self)
StorageAliyun OSSAny S3-compatible (MinIO / OSS / S3)
CacheAliyun TairAny Redis-compatible
ComputeAliyun Function Compute FCHTTP-callable runner (Lambda / SCF)

No self-managed Postgres. This was a major decision in v4: RDS-only across the board removes the “two-track architecture” problem (one for hosted, one for self-host). Cost premium (~30% over self-managed PG) absorbed by the COGS table above.

Mortar’s own binary is stateless — pure Go, runs on a small ECS or any VM. All state lives in the managed services. This means:

  • Hosted SLA bounded by Aliyun’s RDS / OSS / Tair SLAs
  • Self-hosters can scale the Mortar binary horizontally (load balancer in front) with zero changes
  • Cross-region failover is RDS Multi-AZ + multi-region OSS — no Mortar-side replication code to maintain

Implementation phases

The tier table + credit math + 3-mode behavior above is the contract. Code lands in three steps:

PhaseScopeArtifacts
W7.aPlan registry: 5-tier × 7-dim table as Go constants; per-unit prices; mode enuminternal/plan/registry.go + tests
W7.bReal feature/usage Recorder (replace NoopRecorder); credit_ledger table append-onlyfeature/usage/recorder.go, feature/usage/credit.go, model migration
W7.cplan.Gate middleware on write paths; capacity 507/503 mapping; mode-dependent credit-exhaustion handler; noisy-neighbor monitorinternal/plan/gate.go, feature/usage/alerts.go, feature/usage/noisy_neighbor.go
W7.dDashboard endpoint: GET /v1/{tenant}/usage/me returns itemized breakdown + projected month-endapi/usage_endpoints.go

Billing card-charging integration (Alipay / WeChat Pay) and auto-top-up logic are W8+, downstream of this contract.


Open questions

  1. ¥99 credit number — placeholder. Anchor to a competitive point ($25 ≈ ¥175 USD-converted; ¥99 is the iconic “hundred-yuan” tier in mainland-China SaaS pricing). Pick after the first 50 paying customers give us conversion data.
  2. Annual discount — Supabase and other clouds commonly offer 20% off. Defer until ≥ 50 paying projects.
  3. Free editor user → forced Tiny — should they get the same ¥99 credit? Some full-stack clouds do ($25 regardless of plan). Default yes; monitor for Free abuse (one user spinning 100 free projects).
  4. Reserved capacity savings — a planned lever, expected ~30% RDS / Tair discount with 1-year commit. Margins improve to ~85% across all tiers.
  5. Per-tier RAM-aware monitoring thresholds — 15% CPU is OK for Tiny noisy-neighbor flag, but Large tenants legitimately use more. Future: tier-aware thresholds.
  6. Compute overage pricing unit — CPU-min is the user-facing unit; FC bills in CU. Conversion factor pinned at “1 CPU-min ≈ 60 CU at 1 GB memory”, subject to refinement when the first real workload runs.