Back to projects
backendweboperationssecurity 2026

Repalune Multi-Tenant Repair Shop Platform

A multi-tenant SaaS case study for device-repair shops: a Rust and Axum API with schema-per-tenant PostgreSQL isolation, an Astro and Vue staff app, a QR and PIN customer status page with repair approvals, warranty certificates, realtime ticket updates, and a CI-gated Railway release pipeline.

Open Repalune
Technical focus
Rust backend, multi-tenant data architecture, Astro/Vue frontend, and Railway release engineering perspective
Year
2026
Stack
Rust, Axum, sqlx, PostgreSQL, Redis, Astro, Vue 3, TypeScript, OpenAPI, Server-Sent Events, Stripe, Docker, Railway, GitHub Actions, Playwright, OpenTelemetry

Architecture

Multi-tenant repair platform architecture

Repalune separates one public web origin from private Rust services. The Astro server proxies all API traffic, the Rust API enforces tenant, role, branch, and package rules on every request, and PostgreSQL holds one control-plane schema plus one schema per shop.

  1. Stage 1

    Client surfaces

    Shop staff and repair customers use different surfaces served from the same origin.

    • Staff app

      • Astro
      • Vue 3
      • TypeScript
    • Customer status page

      • Astro
      • Server-Sent Events
    Technical details
    Staff app
    Intake, repair board, ticket detail, warranties, inventory, price list, bookings, settings, and owner reporting as Vue islands inside server-rendered Astro pages.
    Customer status page
    QR and PIN access to a no-PII ticket view where customers approve or decline proposed repair items and see live status changes.
  2. Stage 2

    Web edge

    The only service with public ingress.

    • Astro SSR server

      • Astro
      • Node.js
    Technical details
    Astro SSR server
    Guards staff routes by checking the session with the API, resolves the visitor locale, and reverse-proxies API traffic so the browser stays same-origin with httpOnly session cookies and CSRF tokens.
  3. Stage 3

    Rust services

    One Rust binary runs as the API or as a dedicated worker, next to an operator CLI for migrations.

    • Axum API

      • Rust
      • Axum
      • OpenAPI
    • Worker and janitors

      • Rust
      • Tokio
    • Migration and operator CLI

      • Rust
      • sqlx
    Technical details
    Axum API
    A single authorization extractor for membership, role, branch, billing status, and product package, a generated OpenAPI contract, problem+json errors, security headers, and SSE event streams.
    Worker and janitors
    Transactional outbox processing for customer notifications with retry, lease recovery, a dead-letter state, and per-plan quotas, plus scheduled cleanup of trials, tokens, and stalled provisioning.
    Migration and operator CLI
    Applies control-plane and per-tenant migrations to every shop under a deploy lock, and holds operator-only commands for pilots and invitations.
  4. Stage 4

    State and infrastructure

    Private data services with no public ingress.

    • PostgreSQL

      • PostgreSQL
      • sqlx
    • Redis and object storage

      • Redis
      • S3-compatible storage
    Technical details
    PostgreSQL
    A control-plane schema for tenants, users, sessions, billing, the outbox, and the audit log, plus one schema per shop for tickets, customers, devices, warranties, and inventory. LISTEN/NOTIFY drives realtime fan-out.
    Redis and object storage
    Redis backs distributed rate limiting. Private buckets hold ticket attachments, uploaded directly by the browser through presigned URLs, and encrypted database backups.

Key flows

  1. Staff app, Customer status page to Astro SSR server

    All browser traffic, including API calls and event streams, goes to one origin.

  2. Astro SSR server to Axum API

    API traffic is proxied over private networking, so the Rust API is never exposed publicly.

  3. Axum API to PostgreSQL

    Each request pins its tenant schema inside a transaction, and ticket changes publish events that only fire when that transaction commits.

  4. Axum API to Worker and janitors

    Notifications are written to the outbox in the same transaction as the business change they describe.

  5. PostgreSQL to Staff app, Customer status page

    Committed ticket events fan out over SSE, and clients refetch through the normal authorized endpoints.

Outcomes

Impact

  • Built a multi-tenant repair-shop SaaS where every shop lives in its own PostgreSQL schema, with transaction-scoped tenant routing and an isolation test suite proving that data, sequences, and queries never cross between shops.
  • Implemented the repair workflow end to end: intake with printed QR slips, a staged repair pipeline, per-item customer approvals on a no-PII status page, handover rules, versioned warranty certificates, inventory, and owner reporting across 14 locales.
  • Defined a Railway production topology as infrastructure as code, with one public web service and private API, worker, migration, and backup roles, released by a GitHub Actions pipeline that ships a commit only after Rust, frontend, browser-journey, Docker-image, and backup-restore checks pass.

Repalune is a multi-tenant SaaS for device-repair shops. A shop uses it to take in devices, move repairs through a pipeline, get customer approval for each piece of work, hand devices back, and issue warranty certificates. The product is in a limited production rollout at repalune.com.

The technical core is a Rust API with schema-per-tenant PostgreSQL isolation, an Astro and Vue frontend, and a release process on Railway that is gated by an extensive GitHub Actions pipeline.

System Shape

The codebase is a Rust workspace plus a separate frontend application:

  • a library crate that holds the domain model, database access, authorization extractors, services, handlers, router, jobs, and realtime events
  • a migration crate that applies control-plane and tenant migrations across the whole fleet
  • a server binary that runs as the API, as a dedicated worker, or emits the OpenAPI specification
  • an operator CLI for release-safe migrations, migration status reporting, pilots, and invitations
  • an Astro SSR application with Vue islands for the staff app, public booking and intake pages, and the customer status page

The OpenAPI specification is generated from the Rust handlers and committed to the repository. The frontend generates its typed API client from that file, and CI fails if either one drifts from the code.

Schema-per-Tenant Isolation

Each shop gets its own PostgreSQL schema, built from a tenant migration set. A separate control-plane schema holds tenants, users, memberships, sessions, plans, subscriptions, the job outbox, and the audit log.

Tenant SQL is written unqualified, as if there were only one shop. Every request opens a transaction and pins the tenant schema with a transaction-local setting, so a pooled connection can never carry one shop’s schema into another request. Schema names come from a whitelist type and are never formatted into SQL.

Queries are checked at compile time against a template schema, and a committed offline query cache keeps builds reproducible without a database. Tenant migrations apply to the template and to every existing shop under a database advisory lock, so concurrent deploys and new signups cannot race each other.

A dedicated isolation test proves four properties:

  • ticket numbering is per shop, so each shop’s first ticket is number one
  • a ticket from one shop is invisible under another shop’s address
  • the same unqualified query serves every tenant schema
  • concurrent, interleaved writes always land in the correct schema

Request Authorization

Every shop endpoint runs through a single membership extractor. It checks the session, the active membership, and that the shop in the URL matches that membership. A mismatch returns an error and never silently switches shops.

The same gate then applies the remaining rules:

  • suspended shops become read-only, with a narrow billing-recovery path for owners
  • product packages decide which areas a shop can use, and a missing subscription fails closed
  • roles (owner, front desk, service technician, payment desk, counter) map to an explicit permission matrix, and technicians never see customer personal data
  • staff below owner level are restricted to their own branch, and client-supplied branch ids are never trusted
  • a per-shop workflow mode offers a simplified front-desk flow for smaller shops

The authorization types have private fields, and compile-fail tests prove they cannot be constructed outside the gate.

Repair Workflow

Intake records the customer, the device, and the reported problems from localized preset lists, then prints a slip with a QR code. Tickets move through a fixed stage pipeline on a repair board, and the front desk has a queue with a calendar day view.

Diagnosed work is proposed to the customer as individual repair items with optional prices. Proposing an item moves the ticket to awaiting approval in the same transaction as the proposal message and the notification job. Customers approve or decline each item, and staff can send reminders with a cooldown.

Handover goes through one service. Pending customer decisions or reserved parts block it, and an owner override requires a written reason. The platform also covers inventory and stock operations, a shop price list with CSV and Excel import (including legacy text encodings), appointment booking, data import from previous systems, attachments, and owner reporting.

Customer Status Page

The QR token and PIN are stored only as hashes, so no plaintext status link exists after intake. Notification emails point customers back to the printed slip instead of carrying a link.

Public responses contain no personal data and are rate-limited, with lockout on repeated wrong PINs. A correct PIN can remember the device for a limited time, and access is revoked automatically some time after a repair is complete.

Warranties

Repalune issues two kinds of warranty: standalone certificates, and certificates issued automatically at handover when a shop uses both ticketing and warranties.

  • Issuing freezes a versioned snapshot of the terms, issuer, country, and language, and every reprint uses that snapshot.
  • Extensions can only lengthen coverage, need a reason, and use a revision lock.
  • Coverage is never inferred from free text. When no default matches, the certificate starts at zero months and staff must acknowledge that explicitly.
  • The document language comes from the shop’s country, with no silent fallback. Printing stays blocked until the owner configures it.
  • IMEI numbers must pass a length and checksum validation and are never truncated or corrected.
  • Barcodes and QR codes are rendered locally, never by an external service.

Payments Boundary

After a compliance review of German cash-register rules, Repalune deliberately records only the external payment status of a repair. Staff mark a ticket as unpaid, partially paid, or fully paid with a confirmation and a date. Handover requires a current “fully paid” observation or an owner override.

This is enforced on the server, not just hidden in the UI. Service code rejects new payments, refunds, deposits, register activity, and invoice issuance for every shop.

Subscription billing for the SaaS itself runs on Stripe, completely separate from any repair-customer payment flow. The two never share credentials, webhook secrets, or customer objects. Webhooks are signature-verified and reject stale signatures, processing is transactional so a provider retry can finish safely, and exactly-once mutations use fingerprinted idempotency keys.

Realtime and Notifications

Side effects use a transactional outbox. Jobs are written in the same transaction as the business change, and the worker claims them with row-level locking, retry and backoff, stale-lease recovery, and a dead-letter state. Every email goes through one quota check per plan, serialized per shop so concurrent sends cannot overshoot the limit.

Realtime updates use PostgreSQL LISTEN/NOTIFY. Write paths publish a thin event inside the tenant transaction, so it fires only on commit. Each API process fans events out over Server-Sent Events to the staff board and the customer status page, and clients refetch through the normal authorized endpoints instead of trusting event payloads.

Frontend

The Astro server does three jobs. It reverse-proxies the API so the browser stays same-origin, checks the session before rendering staff pages, and resolves the visitor’s locale.

Interactive screens are Vue islands that call the API through a typed client generated from the OpenAPI specification. Non-GET requests carry a double-submit CSRF token.

The interface ships in 14 locales, including right-to-left Arabic, and a CI check requires every locale to carry the same keys. Printed documents and emails reuse the same locale catalogs through generated files embedded in the Rust API. Unit and component tests have coverage gates, and a bundle-size budget runs against every production build.

Railway Deployment

Production runs as one Railway project in the Amsterdam region:

  • a public Astro web service, the only role with public ingress
  • a private API service with readiness health checks
  • a dedicated worker service for the outbox and scheduled jobs
  • a one-shot migration service that must finish and exit before a release continues
  • a nightly backup cron job
  • PostgreSQL, Redis, and two private buckets: one for attachments and one only for encrypted backups

The desired infrastructure state is defined in TypeScript with Railway’s infrastructure-as-code tooling. A human reviews each infrastructure plan before it is applied, and application releases never change infrastructure. Each role receives only the credentials it needs: the web service gets only the API’s private address, and only the backup role can reach the backup bucket.

The backup job writes a PostgreSQL dump, validates it, encrypts it with a public key before upload, and verifies a checksum manifest. It keeps separate daily and monthly retention sets, alongside Railway’s native database backups. The restore tooling refuses to run against anything that is not an explicitly isolated database.

GitHub Actions CI and Release

Every push and pull request runs six independent job groups:

  • Release safety: type-checks the infrastructure definition, tests the release scripts against a mocked Railway CLI, and scans deployment assets for literal credentials.
  • Recovery proof: performs an encrypted backup and an isolated restore against disposable PostgreSQL and S3-compatible storage.
  • Rust build and test: verifies the offline query cache, builds offline, and runs the integration suite against real PostgreSQL and Redis. It also runs rustfmt, Clippy with warnings as errors, and cargo-deny license and advisory checks.
  • Frontend: checks OpenAPI and generated-type drift, locale catalogs and generated email copy, Astro/Vue/TypeScript type checks, coverage, the production build, and the bundle budget.
  • Browser journeys: runs Playwright against production builds with seeded shops, real sessions, and realtime delivery.
  • Docker images: builds the API, web, and backup images, asserts that each runs as an unprivileged user, and smoke-tests each runtime.

Only a push to the main branch releases, and only after every job passes. Releases run one at a time, and a queued commit is skipped if a newer one has already landed on main.

The release job uploads an archive of the exact tested commit and deploys in dependency order: migrations, API, worker, then web. Each step waits for its own deployment to reach a terminal success, and any failure stops the rollout. A final smoke check verifies public health and security headers.

Because the rollout is ordered rather than atomic, migrations and API changes stay backward-compatible with the previously deployed web bundle.

Bench Assistant

The codebase also contains a feature-flagged AI bench assistant for intake drafts, board search, stage changes, and ticket notes, with typed and push-to-talk voice input. The Rust API owns every model call, validates each proposed action against roles, stages, and tickets, and requires explicit confirmation before writing anything. Raw audio and full transcripts are not stored. The assistant is built but switched off in production until its launch.

Technical Perspective

This case study shows backend depth in Rust and PostgreSQL. It covers multi-tenant isolation, authorization design, transactional side effects, realtime delivery, and product rules that encode legal and domain constraints. It pairs that with a production frontend and a release pipeline that treats migrations, backups, and deployment order as tested parts of the system.