Skip to content

How it works

From a declaration to a running operation.

ProWork is a multi-tenant operations kernel. You describe what you run; it gives you a working backend and console. The same modules deploy as one container or as per-module Lambdas, over a single hardened Postgres.

  1. step 1

    Declare

    Set a strategy's manifest in the console. Plain, readable configuration, no scaffolding and no migration to write.

  2. step 2

    Compose

    The kernel wires the modules that strategy needs, over one Postgres, sharing typed contracts across every module.

  3. step 3

    Run anywhere

    You get an API and console instantly, tenant-isolated from the first request, on Docker or AWS Lambda.

The strategies

Six ways to switch on an operation.

Each strategy stands up its own manifest, console surfaces, and API. Turn one on, or compose several into a single workspace.

01

Booking

Take bookings

Appointments that can't double-book.

Reservable resources, real availability in the site's own timezone, deposits at booking. Exclusivity is enforced in Postgres, not hoped for in app code.

CalendarResource gridReschedule / cancelNo-show handling
  • GET/booking/availability
  • POST/booking/appointments
  • POST/booking/appointments/:id/reschedule
booking.strategy
strategy:booking
resource:practitioner// what gets reserved
granularity:30m
horizon:60d
timezone:from_location// DST-correct per site
buffers:{ before: 0m, after: 10m }
exclusivity:postgres_gist// double-booking is impossible
deposit:25%// captured at booking
notify:
confirm
remind @ -24h
remind @ -2h
02

Storefront

Sell things

A catalog and checkout, tenant-isolated.

Products with variants, a cart, and checkout that runs through the built-in payments module. Every price, order and customer stays inside the tenant boundary.

Catalog editorOrdersCustomersPayments ledger
  • GET/storefront/products
  • POST/storefront/carts/:id/checkout
  • POST/payments/intents
storefront.strategy
strategy:storefront
catalog:products
variants:[size, plan]
pricing:per_variant// tax inclusive
checkout:payments.intent// idempotent
fulfilment:manual | shipping
inventory:tracked
03

Records

Manage clients

Client records with column-level policy.

A collection with your fields, related to appointments and invoices. Sensitive columns are gated by policy; row-level security keeps each tenant's records fully separate.

Records browserDetail + timelineSaved viewsImport / export
  • GET/records/clients
  • POST/records/clients
  • GET/records/clients/:id/timeline
records.strategy
strategy:records
collection:clients
fields:[name, phone, email, tags, notes]
policy:column(phone: staff+)// final-stage, key-validated
relations:[appointments, invoices]
isolation:rls_forced// per-tenant, always on
search:pgvector + trigram
04

Subscriptions

Charge monthly

Recurring billing with real dunning.

Plans, trials, proration and a billing cadence that retries failed charges and pauses politely. The subscription engine is part of the kernel, not a plugin you wire up.

PlansSubscribersInvoicesDunning queue
  • POST/payments/subscriptions
  • POST/payments/subscriptions/:id/change-plan
  • GET/payments/invoices
subscriptions.strategy
strategy:subscriptions
plans:
starter · $19 / mo
pro · $49 / mo
clinic · $199 / mo
cadence:monthly | annual
trial:14d
proration:true
dunning:retry(3) -> pause// no silent churn
05

Forms

Collect intake

Intake that becomes a record, then acts.

A form whose submission creates a record, stores an uploaded file, and kicks off a workflow. Configuration, not a form builder bolted onto a spreadsheet.

Form editorSubmissionsFile attachmentsRouting rules
  • GET/forms/:slug
  • POST/forms/:slug/submissions
  • POST/storage/uploads
forms.strategy
strategy:forms
form:intake
fields:[reason, history, consent]
upload:consent.pdf -> storage// signed URLs
on_submit:
create records.clients
start workflow.triage
06

Workflow

Automate steps

Durable automation across your operations.

A workflow that waits for real events, fires timers, and survives a crash mid-run. It parks on 'payment captured', resumes exactly once, and compensates cleanly on a no-show.

Workflow canvasRuns & historyParked stepsRetries
  • POST/workflows/:id/trigger
  • GET/workflows/runs/:runId
  • POST/workflows/runs/:runId/resume
workflow.strategy
strategy:workflow
trigger:appointment.booked
steps:
wait_for payment.captured // durable park/resume
send comms.confirm
after 24h -> comms.remind
on no_show -> charge(deposit)

The kernel

One system, composed, not stitched.

Strategies don't bring their own backends. They compose these modules, which already share typed contracts and a single database.

  • 01

    Bookings

    Availability & reservations

    Timezone-correct slots, resource exclusivity enforced by a Postgres GiST constraint, reschedule and cancel flows.

  • 02

    Payments

    Intents, refunds, payouts

    Idempotent payment intents and refunds behind a provider adapter, so a retry never charges twice.

  • 03

    Subscriptions

    Plans, trials, dunning

    Billing cadence with proration and a dunning ladder that retries then pauses, instead of churning quietly.

  • 04

    Records

    Collections & relations

    Typed collections with relations, column-level policy, and cross-tier reads with no N+1 hydration.

  • 05

    Workflows

    Durable automation

    An executor with durable park/resume, timers, crash recovery, and idempotent compensation.

  • 06

    Comms

    Transactional email

    Templated, tracked transactional email with a cascade of overrides, delivered idempotently.

  • 07

    Storefront

    Catalog & checkout

    Products, variants, carts and checkout that hand off to payments without a separate integration.

  • 08

    Domains & files

    Custom domains, storage

    A domain registry with DNS verification and TLS, plus S3-backed file storage with signed URLs.

Isolation

Every tenant, walled off in the database.

Isolation isn't a filter you remember to add. It's enforced by Postgres on every query, for every table.

BEGIN; SET LOCAL app.tenant_id = 'tenant_b';
Tenant A
•••• 6 rows
denied
Tenant B
client:avaorder:#1042invoice:paid
readable
Tenant C
•••• 12 rows
denied

row-level security is FORCED on every table, including extension tables. the app connects as a non-owner role, so a query bound to one tenant cannot read another's rows.

Deployment

Two runtimes, one kernel.

Run it long-lived in a container, or serverless as per-module Lambdas. The process-model differences hide behind adapters, so your configuration doesn't change.

Long-lived server

The whole kernel runs unmodified in one container next to Postgres and Redis.

  • One compose file: api, console, worker, Postgres, Redis
  • Realtime over WebSocket / SSE included
  • Push to deploy on your own box

one container · one box

apiconsoleworkerpostgresredisrealtime

same modules · same contracts · process differences hidden behind adapters

Under the hood

The guarantees your operation stands on.

ProWork was hardened around a set of non-negotiable properties. These are the ones developers ask about first.

Isolation is enforced, not assumed

Row-level security is FORCED on every tenant table, including extension tables. The app connects as a non-owner role, so it can't be switched off.

Bookings cannot double-book

A Postgres EXCLUDE USING gist constraint is the authority on exclusivity. Locks and availability are optimizations layered on top, never the source of truth.

Availability is DST-safe

Slot math runs in the schedule's own IANA timezone and is compared in timestamptz, so a clock change never books the wrong hour.

Every side effect is idempotent

Payments, comms, and webhooks take an idempotency key, and events emit through an outbox, so a retry never double-charges or double-sends.

Workflows survive crashes

The executor parks durably on events, fires timers, resolves event-versus-timeout deterministically, and compensates idempotently after a restart.

Records mutate atomically

A record and all of its extension tables change in one transaction, and reads hydrate across tiers without an N+1 storm.

See it run on your operation.

Create a workspace, declare a strategy, and watch a real backend come up, no rebuild required.