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

Mortar deployment

Mortar is a stateless Go binary that connects to four managed services for state. It runs identically on a developer’s laptop, a single VPS, or a horizontally-scaled production cluster — what changes is which backends you point it at.

This doc covers: required runtime, the four backends and their swappable implementations, a self-host quickstart, and the hosted-vs-self-host architecture.


Required runtime

  • Go 1.25+ (build only; the released binary is static)
  • Postgres 16+ with pgcrypto extension installable
  • Redis-compatible cache (any version that speaks RESP)
  • HTTP-callable function runner (or local subprocess for dev)
  • S3-compatible object storage (or local disk for dev)

The Mortar binary runs on any Linux x86_64 / ARM64 with zero system dependencies (musl-static). Memory: ~64 MB RSS at idle. CPU: scales with request rate.


Backend compatibility matrix

Mortar’s five primitives (database, cache, storage, compute, network) each have a swappable interface (internal/primitive/{database,cache,storage,compute,network}). Implementations live under internal/backend/<vendor>/.

Database (primitive/database)

BackendHostedSelf-hostNotes
Self-hosted PostgreSQL 16+Any VM, Docker, K8s. Requires pgcrypto.
AWS RDS PostgreSQL 16+All Mortar features supported (RLS, pg_notify).
AWS Aurora PostgreSQL 16+Compatible mode; faster scale-out.
Aliyun RDS PostgreSQL HAMortar Cloud’s choice; available in mainland China + overseas.
Aliyun PolarDB PostgreSQLAurora-equivalent on Aliyun.
Tencent Cloud TDSQL-C / CDB PostgreSQL
Google Cloud SQL PostgreSQL
Neon / Supabase PostgresYes, you can run Mortar on top of Supabase if you want.
CockroachDB / YugabyteDBNot Postgres-protocol-complete enough for our RLS.

Required Postgres features:

  • pgcrypto extension (for gen_random_uuid())
  • Row-Level Security (Postgres-native, all versions)
  • pg_notify / LISTEN (Postgres-native)
  • JSONB + GIN indexes (Postgres-native)

Mortar does not require any non-allowed RDS extension. See the Optional extensions section for what you can opt into without breaking Mortar.

Storage (primitive/storage)

BackendHostedSelf-hostNotes
LocalDiskDev / single-VPS only. No signed URLs.
Aliyun OSSMortar Cloud’s choice.
Tencent Cloud COS(planned)
AWS S3(planned)
MinIO / any S3-compatibleSelf-host; OSS-compatible signature.

Cache (primitive/cache)

BackendHostedSelf-hostNotes
InMemoryCache✓ (dev)Single-process; no pub/sub across nodes.
Aliyun Tair (in-memory)Mortar Cloud’s choice. Redis-compatible.
Tencent Cloud Redis(planned)
AWS ElastiCache(planned)
Self-hosted Redis / KeyDB / DragonflyAny RESP-speaking server.

Compute (primitive/compute)

BackendHostedSelf-hostNotes
LocalSubprocess✓ (dev)Spawns node / deno in a subprocess.
Aliyun Function Compute FCMortar Cloud’s choice. Per-CU billing.
Tencent Cloud SCF(planned)
AWS Lambda(planned)
Cloudflare WorkersRoadmap; cold-start latency advantage.
Self-hosted Deno runtimeAny Deno-compatible HTTP-invokable runner.

Self-host quickstart

# Pull the binary
curl -L https://github.com/liamxujia/appunvs/releases/latest/download/mortar-linux-amd64 -o mortar
chmod +x mortar

# Set the four backends + auth secret
export MORTAR_DB_URL="postgres://user:pass@host:5432/mortar?sslmode=require"
export MORTAR_REDIS_URL="redis://host:6379/0"
export MORTAR_STORAGE_BACKEND="local"   # or "oss" / "cos" / "s3" / "minio"
export MORTAR_STORAGE_LOCAL_ROOT="/var/mortar/files"
export MORTAR_JWT_SECRET="$(openssl rand -hex 32)"

# Apply schema migrations (explicit — the server never migrates at boot)
MORTAR_DB_URL="$MORTAR_DB_URL" ./migrate up   # or: make migrate-up

# Boot — verifies the schema is at the expected version, then serves
./mortar
# → http://localhost:8080/health

Schema is owned by versioned goose migrations (under internal/primitive/database/migrations/) and applied via the explicit migrate up step. The server does NOT change schema at boot — it only verifies the DB is migrated and fails fast otherwise, so a deploy can never silently alter the schema. GORM remains the ORM for all CRUD.

That’s it. The binary will:

  1. Connect to Postgres, ping it
  2. Verify the goose schema version matches what the binary expects (fatal with “run make migrate-up” if the DB is behind)
  3. Verify the DB role can’t bypass RLS (NOSUPERUSER / NOBYPASSRLS)
  4. Connect to Redis (best-effort ping)
  5. Mount the HTTP API on MORTAR_LISTEN (default :8080)

The migration itself (run once per deploy, before booting the new binary) creates pgcrypto, the tables, RLS policies, and the pg_notify trigger.

  • Postgres: managed RDS or equivalent. Multi-AZ for HA.
  • Reverse proxy: nginx / Caddy / ALB in front for TLS.
  • Mortar binary: 2+ replicas behind a load balancer (the binary is stateless — replicas are interchangeable).
  • Storage: object storage if you’ll have any non-trivial file uploads. LocalDisk is fine for proof-of-concept.
  • Backup: trust your Postgres provider’s PITR. Mortar holds no state outside Postgres + the storage backend.

Skip for production self-host

MORTAR_NOISY_NEIGHBOR_MONITOR is off by default for self-host (set =on only on multi-tenant deployments). Single-tenant self-hosters don’t have noisy neighbors by definition.


Hosted vs self-host architecture

Same binary, different wiring:

SELF-HOST (single tenant, simplest case):

  ┌─────────────────────────┐
  │  Mortar binary          │ ─── connect ───▶  User's Postgres
  │  - HTTP API             │                    (RDS / Aurora /
  │  - Realtime listener    │                     self-hosted PG)
  │  - Plan gate            │
  │  - Usage recorder       │ ─── connect ───▶  User's Redis
  │  (single process, often │                    (ElastiCache /
  │   single-tenant)        │                     self-hosted)
  └─────────────────────────┘ ─── connect ───▶  User's S3 / MinIO
                              ─── invoke  ───▶  User's Lambda /
                                                 Deno / FC
MORTAR CLOUD (multi-tenant hosted production):

  ┌─────────────────────────┐
  │  Mortar binary x N      │ ─── connect ───▶ Aliyun RDS PG
  │  (behind ALB)           │                  (shared pool +
  │  + Noisy-neighbor mon   │                   RLS multi-tenant)
  │  + Plan gate / Recorder │
  └─────────────────────────┘ ─── connect ───▶ Aliyun Tair
                              ─── connect ───▶ Aliyun OSS
                              ─── invoke  ───▶ Aliyun FC

The Mortar binary is bit-for-bit identical in both deployments. The difference is environment variables + the noisy-neighbor monitor toggle.


Optional extensions

Mortar doesn’t require them, but they don’t conflict either — turn them on if your app needs them.

PostGIS (geographic / spatial)

Supported on every major managed Postgres:

ProviderVersionEnable
AWS RDS3.4.xCREATE EXTENSION postgis;
Aliyun RDS3.xSame; bonus: Aliyun Ganos (PostGIS-compatible)
Tencent Cloud CDB3.xSame
Aurora PG3.4.xSame
Google Cloud SQL3.xSame
Self-hosted PGanyapt install postgresql-16-postgis-3 etc.

Mortar’s app_rows.data JSONB doesn’t conflict with PostGIS columns. You can either store coords as JSONB paths (data->'$.lat') or add explicit geometry columns via raw migration and use PostGIS native operators on them.

TimescaleDB / pg_partman

Self-host friendly. Some RDS variants restrict; check your provider. Mortar doesn’t depend on either.

Custom user-installed extensions

Mortar’s goose migrations touch only its own tables (projects, api_keys, app_users, app_tables, app_columns, app_rows, app_files, …). Anything else in your database — your tables, your extensions, your views — Mortar ignores.


Migration from Mortar Cloud → self-host

Coming soon. Roadmap:

  1. mortar export --project=<id> — dumps all per-project tables + storage objects + metadata to a tarball
  2. mortar import --target-db=<dsn> on the new self-hosted instance — loads the tarball, runs Setup, tenant ID preserved

This is a key open-source guarantee: never lock you in. You can always leave Mortar Cloud and run the same binary against your own infrastructure.