Skip to main content

pawabase-iac

Pawabase as code. Define a whole Pawabase backend in Python (tables, access policies, flows, routes, events, schedules, webhooks, buckets, email templates, roles), check it before anything is sent, see what a deploy would change, rehearse it against a simulated environment, and deploy it with an API key.

pip install pawabase-iac

pawabase init          # pawabase.toml, infra/app.py, .env.example
pawabase validate      # offline: every definition and every reference between them
pawabase emulate       # a pretend deploy: nothing real is touched, no key needed
pawabase plan          # what a deploy would change in the real environment (reads only)
pawabase deploy        # apply it; needs a deploy API key
  • Everything is code. A table, a policy, a flow with branches and loops, a cron schedule: all Python you can review, test and version.
  • Checked before it is sent. Names, field types, constraints, policy conditions, flow graphs, template syntax, block settings and the references between all of them are validated offline, with the server's own rules.
  • A plan before a change. plan and deploy --dry-run show what would be created (+), changed (~, with the fields that differ) or removed (-).
  • Safe by default. Pushing needs a secret API key; merge mode never deletes what your code does not mention; production asks first; secrets never sit in the code.

Contents

Quick start · Defining things · Policies · Flows · Commands · Deploying · Emulate · Using it in CI · Examples · Development · Limits

Quick start

mkdir shop && cd shop
pawabase init

pawabase.toml says where to deploy (the key does not live here):

url = "http://localhost:8080"     # your Pawabase gateway
environment = "development"      # the environment to deploy to
entry = "infra"                  # a file or a folder of .py files holding your definitions

infra/app.py is a small working backend. Make the API key (Studio → API keys → New key → Secret, preset Deploy and configure) and put it in .env:

echo 'PAWABASE_API_KEY=pb_sk_...' >> .env
pawabase validate && pawabase emulate && pawabase deploy

Edit a definition and run pawabase plan to see exactly what changes before you deploy it.

Defining things

Definitions register themselves as they are made. Files in your entry folder can import each other (from infra.policies import mine), and each file runs once. Pass a definition wherever a name is expected (resource=todos, handler=summary, run=nightly, operations=crud(mine)) and it is replaced by its name, so a rename follows and validate can see the dependency.

from pawabase_iac import (
    Allow, Array, BelongsTo, Boolean, Desc, Email, Index, Integer, P, Policy, RateLimit, Role, String, Table, Where, crud,
)

Role("teacher", permissions=["grades.write"], description="Teaching staff")

mine = Policy("mine", P.owner("user_id"))
staff = Policy("staff", P.role("admin") | P.role("teacher"))

students = Table(
    "students",
    fields=[
        String("user_id", unique=True),
        String("admission_number", required=True, unique=True, max_length=20),
        Email("email"),
        Integer("age", minimum=3, maximum=25),
        String("status", enum=["active", "graduated", "withdrawn"], default="active"),
        Array("guardian_ids", of=String("guardian_id")),
    ],
    operations=crud(staff, get=Allow(mine.condition | staff.condition), delete=Allow(P.role("admin"))),
    indexes=[Index("status", Desc("age"), where=[Where("status", "!=", "withdrawn")])],
    rate_limit=RateLimit(120, 60),
    realtime=True,
)
Kind Class What you set
Table Table (also Resource) typed fields, operations with a policy each, relations (BelongsTo, HasMany), indexes (Index, Desc, Where), rate limit, caching, owner field, realtime, event capture
Fields String, Text, Integer, Number, Boolean, DateTime, Date, UUID, ULID, Email, URL, JSON, Array, Object, Ref required, nullable, default, unique, indexed, enum, min_length/max_length, minimum/maximum, pattern, read_only, write_only, nested fields and items
Shapes Schema, Transformer reusable data shapes; omit / pick / set / rename / case on the way out
Access Role, Policy roles and their permission names; policies as Python expressions
Logic Flow, Route a graph of blocks; a custom HTTP endpoint that runs a flow
Events Subscription, Schedule, Webhook, InboundHook react to events, run on cron or an interval, call out, be called (signed)
Files Bucket per-action access policies, accepted types, size, signed URLs, image transforms, caching
Email MailTemplate subject / html / text with {{ variables }}, status, categories

Secrets are never written in code: Webhook(..., secret=env("ERP_SECRET")) reads the variable when you deploy and refuses to deploy if it is unset.

Policies

from pawabase_iac import P, Policy, all_of, any_of

Policy("owner_or_staff", P.owner("user_id") | P.role("admin"))
Policy("paid_and_verified", P.authenticated() & (P.auth.plan != "free") & P.auth.email_verified.truthy())
Policy("family", (P.record.student_user_id == P.auth.user_id) | P.record.guardian_ids.contains(P.auth.user_id))
Policy("not_suspended", ~P.record.suspended.truthy())

P.auth, P.record, P.input, P.request, P.env and P.credential are paths into what a policy can see. Comparing a path builds a condition (==, !=, >, >=, <, <=, .in_(), .not_in(), .contains(), .startswith(), .endswith(), .matches(), .exists(), .truthy(), .empty()); &, | and ~ combine them. The shorthand checks are P.authenticated(), P.role(), P.permission(), P.owner(), P.service(), P.kind(), P.scope() and P.aal(). A condition has no truth value in Python, so and / or / not raise instead of silently dropping half a policy.

Flows

from pawabase_iac import Flow, P, blocks, ctx

invoice = Flow("issue_invoice", description="Invoice a student")
start = invoice.add(blocks.trigger.http(method="POST", policy="finance_staff").named("start"))
student = invoice.add(blocks.resource.get(resource="students", id=ctx.input.student_id).named("student"))
missing = invoice.add(blocks.error.raise_(status=404, message="No such student").named("missing"))
fees = invoice.add(blocks.resource.list(resource="fee_structures", filters={"grade": ctx.steps.student.output.grade}, limit=1).named("fees"))
total = invoice.add(blocks.transform.summarize(value=ctx.steps.fees.output.data[0].items, operation="sum", field="amount").named("total"))
big = invoice.add(blocks.control.if_(P.steps.total.output > 100000).named("big"))
review = invoice.add(blocks.event.emit(event="invoice.needs_review", payload={"total": ctx.steps.total.output}).named("review"))
done = invoice.add(blocks.response.return_(status=201, body={"total": ctx.steps.total.output}).named("done"))

start >> student
student.missing >> missing
student >> fees >> total >> big
big.true >> review >> done
big.false >> done

a >> b follows the next handle; a.true >> b, a.error >> b or a.on("done") >> b follow others. flow.chain(a, b, c) adds and links in one go; flow["id"] returns a node you named with .named("id").

All 69 flow blocks the server knows are available as typed functions, one module per family, every option documented with its default, limits and allowed values: auth · cache · control (if, switch, foreach, while, retry, timeout, set, merge, delay, stop) · crypto · db · error · event · flow · http · json · log · mail · math · metric · policy · queue · realtime · resource · response · secret · storage · time · transform (map, filter, sort, group, unique, flatten, slice, batch, pick, rename, summarize, template) · trigger (http, event, schedule, webhook, resource, job, realtime, manual) · util · validate · webhook.

The rules the server enforces (and validate checks)

  • Templates are lookups. {{ steps.load.output.id }} and the filters default, upper, lower, length, int, float, str, bool, first, last, keys, json; nothing else. No arithmetic, no comparison, no path after a filter. Compute with blocks.math.calculate; decide with blocks.control.if_(P.steps.x.output.n > 3). A value that is exactly one template keeps its type (a list stays a list).
  • Conditions use P (control.if_, control.while_, transform.filter): P.input.x, P.steps.<id>.output.y, P.vars.z, P.item.w.
  • What input holds depends on how the flow started: an HTTP flow gets the body (POST /flows/v1/<name> for a trigger with no path) or {params, query, body, headers} (a Route); an event flow gets {"event": {name, payload}} (a table's changes are <table>.created|updated|deleted with the row at payload.record); a schedule gets {scheduled_at} or its payload; an inbound hook gets {hook, headers, body}.
  • Loops. ctx.item and ctx.index are the current element and a nested loop shadows the outer item: keep what you need with blocks.control.set(values={...}) and read it from ctx.vars. A loop's output is the list of its body's last outputs.
  • validate.schema is strict: declare every optional input as well, or it is refused.
  • Debug a deployed flow with POST /platform/v1/envs/<env>/flows/<name>/run {"input": {...}}: it returns the result, or a step-by-step trace.

Commands

Command
pawabase init start a project here
pawabase validate check every definition and reference; contacts nothing
pawabase plan compare your code with the environment; writes nothing
pawabase deploy apply the plan (--dry-run, --yes, --mode, --force-secrets, --no-migrate)
pawabase emulate rehearse a deploy against a simulated environment (--reset)
pawabase whoami show the settings and test the API key

Shared options: -e/--env, --url, --entry, --json. Exit code 0 on success, 1 on a failure or a refusal, 2 when --yes was needed but not given.

Deploying

A key is required to push. deploy refuses without PAWABASE_API_KEY (in .env or the environment), and accepts only a secret key (pb_sk…). Use a deploy key: Studio → API keys → New key → Secret → preset Deploy and configure. It may change tables, policies, routes, flows, mail templates, roles, buckets, webhooks, hooks, subscriptions and schedules, and nothing else: not users, not API keys, not the database console, not the environment itself. The gateway refuses every other request from it.

Modes.

  • --mode merge (default) creates and changes what your code defines and leaves everything else alone. Anything in the environment that your code does not mention is shown as ? in the plan.
  • --mode override makes the environment match your code exactly and deletes the rest. It asks first (or needs --yes).

What happens. Everything is validated first; if an env("…") variable is unset nothing is sent. Definitions are applied in dependency order (roles, schemas, transformers, policies, tables, flows, buckets, templates, routes, subscriptions, webhooks, hooks, schedules). After a table is created or changed its table is migrated: columns are created or added, never dropped. The deploy stops at the first refusal and tells you how many later changes were not attempted; what was applied stays applied, so fix it and deploy again. Production environments (production, prod, live) need --yes when not interactive.

Secrets. Webhook and InboundHook secrets are write-only: sent when the definition is created or otherwise changes, never compared. Rotate one with --force-secrets.

How it compares. The server fills in defaults and ids, so the comparison asks only whether what you wrote is present in what the server returns (an empty value matches a missing one). Fields the server derives when unset (a table's table name) are not compared.

Emulate

pawabase emulate runs the same validation and planning against a simulated environment kept in .pawabase/emulator-<env>.json. It never touches a server and needs no key. It prints the plan, applies it to the pretend environment, and shows what the result would serve: every REST endpoint with its policy, custom routes, hooks, schedules, webhooks, subscriptions and buckets. Run it again after editing to see only what changed; --reset starts empty; --mode override shows what a destructive deploy would remove.

Using it in CI

- run: pip install pawabase-iac
- run: pawabase validate                       # on every pull request
- run: pawabase plan                           # show the diff in the log
  env: { PAWABASE_URL: ${{ vars.PAWABASE_URL }}, PAWABASE_API_KEY: ${{ secrets.PAWABASE_DEPLOY_KEY }} }
- run: pawabase deploy --yes                   # on merge to main
  env: { PAWABASE_URL: ${{ vars.PAWABASE_URL }}, PAWABASE_API_KEY: ${{ secrets.PAWABASE_DEPLOY_KEY }}, ERP_SECRET: ${{ secrets.ERP_SECRET }} }

PAWABASE_URL and PAWABASE_ENV override pawabase.toml.

Examples

  • examples/store: a small shop (tables, a checkout flow with a loop, retry and error paths, a receipt flow, switch-based ticket routing, subscriptions, a webhook, a signed inbound hook, buckets, a mail template).
  • examples/school: a complete school management system: 11 roles, 22 policies, 43 tables, 25 flows, routes, subscriptions, schedules, signed payment webhooks, buckets and email templates (136 definitions) with a smoke test that runs the real flows against a deployed environment and its own offline tests.

Development

pip install -e '.[test]'
pytest                              # the library, offline

The conformance tests compare every definition's request body with the server's own request models, run flows through the server's flow validator and check the generated block catalogue against the running server's. They run where pawabase_server is importable:

cd ../pawabase && PYTHONPATH=../pawabase-iac uv run python -m pytest ../pawabase-iac/tests/test_server_conformance.py

Regenerate the flow blocks when the server gains some:

cd ../pawabase && uv run python ../pawabase-iac/scripts/generate_blocks.py
pawabase_iac/
  core/          base Definition, env(), the registry
  expr/          P (policy conditions) and ctx (flow templates)
  tables/        fields, Table, Schema, indexes, relations
  policies/      Policy, Transformer, condition checks
  access/        Role
  flows/         Flow builder, graph checks, layout, catalogue.py and blocks/ (generated)
  api/           Route
  events/        Subscription, Schedule, Webhook, InboundHook
  storage/       Bucket
  mail/          MailTemplate
  validate/      cross-definition reference checks
  engine/        plan, apply, diff, what-it-serves
  environments/  Remote (the Platform API) and Emulated (in memory)
  project/       pawabase.toml, .env, importing your definitions
  cli/           the pawabase command

Limits

Environment-level settings are not managed from code yet: auth providers and settings, secrets, API keys, firewall rules, domains, alarms, dashboards, backup schedules, and mail and storage connection settings. Users are not managed either (give people roles in Accounts). The twelve kinds above are.

License

BSD 3-Clause. See LICENSE.

Metadata

Release files for pawabase-iac 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pawabase-iac 0.1.0
File Size Uploaded Provenance Actions
pawabase_iac-0.1.0.tar.gz 107.5 kB Without provenance Details for pawabase_iac-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pawabase-iac 0.1.0
File Interpreter ABI Platform Actions
pawabase_iac-0.1.0-py3-none-any.whl Python 3 none any Details for pawabase_iac-0.1.0-py3-none-any.whl

Total release size: 196.1 kB

Release files / pawabase_iac-0.1.0.tar.gz

Download URL pawabase_iac-0.1.0.tar.gz
Size 107.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5f4512ee5a8d07e75deacf67825602a56db7bc3d1e14ba31cf6cebc592004bd2
BLAKE2b-256 checksum
How to use checksums
0db68774ffd0db5dd66ecd951ed095cb6a28868a900aa155a6e5ab65e00d51d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pawabase_iac-0.1.0-py3-none-any.whl

Download URL pawabase_iac-0.1.0-py3-none-any.whl
Size 88.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
40a921d410952af62932940ce47d96c34b94e5542239521c42e936034c0a6183
BLAKE2b-256 checksum
How to use checksums
a75e28397ad498fd2f8f68ac118f35169bab1f50f48ccf7768a35062d1abc44d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for pawabase-iac 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page