Skip to content
Menu

Documentation

Upgrading to Public API 1.0

Public API 1.0 gives modules one namespace, sandloom.sdk, and a declarative form for routes, events and contracts. The migration replaces the earlier internal API without compatibility aliases: an old import path stops working and the error names the new one. This page lists every change a module author meets, the replacement, and the check that reports the old form. The full name-by-name table is generated by tools/packaging/public_api_migration.py.

Old names on this page

This is the one page that names the removed and renamed APIs, to map them to their replacements.

High impact

Imports move to sandloom.sdk

A module imports only public sandloom.sdk modules, generated contract stubs and DTOs, and its own package (SANDLOOM-C002, tests: SANDLOOM-C013). Every existing name keeps its name and all its parameters at its new home, except the renames and removals below.

Old home New home
sandloom.core.di (sessions, dependencies) sandloom.sdk.web
sandloom.core.di (effects: record_audit, on_commit, before_commit, durable_inbox, operation_key, run_async) sandloom.sdk.effects (record_audit, on_commit also from sandloom.sdk)
sandloom.core.di (events: publish_event, publish_event_versions, EventEnvelope, PoisonEventError) sandloom.sdk.events
sandloom.core.di (tenancy: bind_tenant, unscoped, scoped, tenant_filter, current_tenant_id) sandloom.sdk.tenant
sandloom.core.di (security: token, revocation and block helpers, password hashing) sandloom.sdk.security
sandloom.core.di (cache, notifier, get_core, settings) sandloom.sdk.infra
sandloom.core.di (sagas: start_saga, saga_status) and sandloom.core.module_api (Saga, step, local_step) sandloom.sdk.sagas
sandloom.core.di exceptions sandloom.sdk.errors
sandloom.core.module_api (define_module, ModuleAPI) and sandloom.core.descriptors sandloom.sdk.module (define_module also from sandloom.sdk)
sandloom.core.module_api (contract_method, the contract runtime) sandloom.sdk.contracts
sandloom.core.orm sandloom.sdk.data (Base, TenantScoped also from sandloom.sdk)
sandloom.core.projections sandloom.sdk.projections
sandloom.core.facade sandloom.sdk.infra
sandloom.shared.authz sandloom.sdk.authz (the beginner atoms also from sandloom.sdk)
sandloom.shared.guards.auth, sandloom.shared.guards.platform sandloom.sdk.web
sandloom.core.testing, sandloom.core.contract_testing, sandloom.core.testing.fixtures sandloom.sdk.testing, sandloom.sdk.testing.fixtures
sandloom.core.app_factory.create_app sandloom.sdk.app
sandloom.core.migrations.run_module_migrations sandloom.sdk.migrations

sandloom docs <name> prints any name's new home and signature.

Route policy: no operation classes

ADR-0009 removed route classes and freshness knobs. Every route is authenticated unless it says public(reason=...), and every authenticated route reads the session record live (Route policy and sessions).

Removed Replacement
operation_class("P") public(reason=...); issuers public(reason=..., issues_tokens=True)
operation_class("R" \| "RS" \| "W" \| "F" \| "A") nothing: the policy is derived; risk is declared at its source
strict_revocation(), revocation_freshness(...), MAX_REVOCATION_STALENESS_SECONDS nothing: every route is strict
AUTH_REVOCATION_STRICT_PATH_PREFIXES nothing (in config_tiers.REMOVED, inert)
revocation_at_commit() automatic on writes; at_commit() for a non-write route that commits
the class-F guard check (FinancialGuardError) mandatory money dedupe (SANDLOOM-R009, SANDLOOM-R010) and SANDLOOM-B019
assert_operation_classes(app), operation_classes(app) assert_route_snapshot(app), route_policy(route)

A route that is neither authenticated nor public fails at startup (SANDLOOM-B008). Retired codes: SANDLOOM-B003, SANDLOOM-B004, SANDLOOM-C016.

Billing atoms name their account

Subscribed, SubscriptionActive, Credits, Feature and WithinLimit take a required of= (SANDLOOM-B017):

Old New
Subscribed() (default tenant_type="user") Subscribed(of=Account.user())
Subscribed(tenant_type="organization", org_param="org_id") Subscribed(of=Account.org(param="org_id"))
SubscriptionActive(tenant_type="organization") on a route without the parameter SubscriptionActive(of=Account.org(param=None))
Feature(key), WithinLimit(key, used) Feature(key, of=Account.user()), WithinLimit(key, used, of=Account.user())

An organization route with of=Account.user() needs a reason (SANDLOOM-C027). See Authorization.

Billing API behaviour change: without org_id, /subscription, /credits, checkout and portal use the caller's personal account. A member of exactly one organization no longer gets that organization's account implicitly: pass org_id.

Commands

Old New
sandloom-scaffold <name>, sandloom scaffold sandloom add module <name> (how-to)
sandloom-scaffold saga <module> <name> sandloom add saga <module> <name> (how-to)
sandloom-migrate <command> sandloom migrate <command> (how-to)
sandloom-migrate revision <module> -m "..." --autogenerate sandloom migrate new <module> "..." (adds row-level security) (how-to)
adding the module to SANDLOOM_MODULES / the default module list and testpaths nothing: modules are discovered (app modules statically, module packages by their sandloom.modules entry point); SANDLOOM_MODULES selects a subset

Medium impact

Declarative module surface

The level-2 forms still work; level 1 is shorter and is what the generators write (Module internals).

Old (level 2) New (level 1)
define_module(__name__, events=EVENT_SCHEMAS, setup=...) with hand-written JSON Schemas class E(Event, channel="m.e.v1") in events.py; discovered
await publish_event("m.e.v1", {...}) await E(...).publish()
core.register_event_consumer(channel, handler, name=..., bind_tenant_from="tenant_id") in setup @on(View) on async def f(event: View) -> None in consumers.py
@classmethod @contract_method(...) methods and core.register_service(...) @contract("x.y.v1") class with plain async def methods; @command(idempotency_key=...)
ContractRequirement(...) in consumes= and core.get_service("x.y.v1") quota = Contract(X_Y_V1) at module level (consumes is derived); Contract(X_Y_V1, optional=True).client() when absence is a correct answer
step("s", call="billing.credits.v1.spend", ...) step("s", call=credits.spend, ...)
QueryProjectionDescriptor + consumers + monotonic_upsert + ProjectionRebuilder Projection(...) with SourceGroups and mappers
router = APIRouter(...) with auth and tenant dependencies router = module_router()

Sessions

Old New
db: DbSession parameters threaded through services db() inside services (DbSession stays, advanced)
async with db_session(): async with open_uow():
contract_session() inside a contract method db()
a session passed to asyncio.gather children each child opens open_uow() (SANDLOOM-R013)
a second writing session in one request refused at its first write (SANDLOOM-R007); writes("x") or allow_multi_db_write(...)

Idempotency

Every authenticated write that carries Idempotency-Key is deduplicated automatically, and the retry gets the stored status only (mode S). A client that relied on a replayed body re-reads the resource with a GET, or the route declares replay_safe(reason=...) after review. Explicit idempotency_guard(...) calls keep all their options (Make a write idempotent).

Tests

Old New
make_user_headers(...) as_user(...) (same parameters and result)
deliver_event(channel, payload, ...) deliver(event_or_channel, payload=None, ...) (same parameters; also takes an Event)
capture_effects(...) for published events published(E)
a hand-built fake service bound in the registry fake_contract(REF, impl)
-p sandloom.core.testing.plugin -p sandloom.sdk.testing.plugin

See the Testing reference.

Low impact

Old New
Depends(get_current_user_id) user: CurrentUser (user.user_id); the function stays as sandloom.sdk.web.current_user_id
function-local imports in modules top-level imports (SANDLOOM-C021); a marked optional dependency is allowed
routes.snapshot.json regenerated by hand regenerated by sandloom generate with a compatibility check

Verify

uv run sandloom check        # reports every old import and form with its fix
uv run sandloom test --full  # tests, gates and generated files