Skip to content

The Extension SDK & Testkit

Every Vectis extension is written against two packages that ship with the engine:

  • vectis_sdk — the sole sanctioned import surface for extensions. Extensions import from vectis_sdk.* and nothing else; the extension→core import allowlist is {}.
  • vectis_testkit — reusable pytest fixtures (a pytest11 plugin) and five contract suites an extension runs to prove it is a well-behaved member of the platform.

Both landed in the True Extension Platform wave.

Where this fits

The open developer surface (Apache-2.0)

vectis_sdk and vectis_testkit are the open, permissively-licensed developer surface (both Apache-2.0) fronting a commercial core (Article 7 of the project constitution). Along with the public vectis-ext-example and this documentation, they are the only artifacts published to public PyPI — the vectis engine is proprietary and is never published publicly.

Both packages carry their own [project] metadata + py.typed and are buildable as wheels. The vectis engine is an optional [engine] extra, not a hard dependency: pip install vectis-sdk resolves on public PyPI without the (non-public) engine and gives the versioned interface + typing + contract reference. Where the licensed core is present (cloud, licensed self-host, or the developer distribution), install vectis-sdk[engine] / vectis-testkit[engine] to back the facade at runtime. Extension wheels, by contrast, depend on vectis-sdk itself, pinned to the core version they target — see Extension Packaging & Extraction.

vectis_sdk — the import surface

An extension imports the submodule it needs:

from vectis_sdk.extension import ExtensionManifest, VectisExtension
from vectis_sdk.db import get_session_factory
from vectis_sdk.strategies import PaymentStrategy

Direct vectis.core.* / vectis.modules.* imports inside an ext_* package are forbidden — the allowlist in scripts/extension_import_allowlist.json is {}, and the check-extension-imports gate (part of make check) fails on any core import that is not routed through the SDK. Every first-party extension was codemodded to this zero-allowlist state, so vectis_sdk is the only boundary that exists.

Lazy facade — importing a module pulls zero core; resolving a symbol needs the engine

The SDK is a lazy PEP-562 facade: import vectis_sdk.<mod> re-exports the engine through a module-level __getattr__, so importing a submodule drags in zero core — the engine is only touched when you resolve an attribute off it. This is what lets pip install vectis-sdk (no [engine] extra) install and import on public PyPI without the non-public engine present: you get the versioned interface and typing, and a symbol resolves only once the engine is installed (the [engine] extra, or an in-tree checkout).

Two consequences for authors:

  • Import the submodule you need, not the package: from vectis_sdk.db import get_session_factory, never import vectis_sdk expecting the surface to be attached.
  • models, catalog, and services are the exceptions — they are eager: resolving them builds the full ORM mapper closure, so they require the engine. vectis_sdk.strategies likewise triggers core's built-in email/SMS strategy registrations on attribute access.

Modules and tiers

The SDK is 16 submodules split across two stability tiers. The exported name set per module is snapshotted in backend/vectis_sdk/api_surface.json.

Tier 1 — frozen

Names exported here change only with a major version. Additions are minor versions; removals and signature-narrowing require a major bump.

Module What it exposes
cache get_redis — the shared Valkey (Redis-protocol) handle.
config Settings, get_settings, get_channel_config, resolve_strategy_config, and the secret-field encrypt/decrypt helpers.
db Base, TimestampMixin, money_column, get_engine, get_session, get_session_factory.
events Event, EventBus, event_bus, publish_event, validate_event, SchemaValidationError.
extension The authoring core: ExtensionManifest, VectisExtension, ExtensionProtocol, ExtensionRegistry, ChannelExtension, ExtensionInstallState, the register_admin_* helpers, UiFormResult, LedgerEntry, EXTENSION_CATEGORIES, DuplicateRegistrationError.
fulfillment run_with_fallback, seed_carrier_methods.
http RequestContext, resolve_channel, AuthenticationError, AuthorizationError.
migrations Per-extension Alembic helpers — migrate_extension, owned_tables_for, assert_no_schema_drift, find_migrations_path, the drift/namespace guards, and more.
payment TransactionResultType, account_scope.
services AuthService, OrderService, SettingsService, SavedPaymentMethodService.
storage FileStorageStrategy, S3Service.
strategies The full strategy ABC surface (77 names) — PaymentStrategy, ShippingRateStrategy, AICompletionStrategy, AgeVerificationStrategy, EmailDeliveryStrategy, address-validation, fraud, e-signature, fee-calculator, and their input/result dataclasses.
temporal get_temporal_client, close_temporal_client.
testing The contract helpers — assert_imports_clean, assert_manifest_valid, assert_lifecycle_clean, assert_strategy_satisfies, assert_webhook_vectors, WebhookVector, assert_schema_composes — re-exported from vectis_testkit.

Tier 2 — read-stable

ORM classes and read projections, re-exported for reading. Attributes and columns may be added in a minor version, but the exported name set is never removed or retyped without a major bump. Writing through these models from an extension is unsupported API.

Module What it exposes
models 29 ORM classes for reads — Account, Cart, Channel, Customer, Order, OrderLineItem, Product, ProductVariant, PaymentTransaction, Setting, Shipment, User, and the rest.
catalog PublicCatalogProjection, to_ai_product — read projections over the catalog.

The check-sdk-frozen gate

The semver policy is enforced, not just documented. scripts/check_sdk_frozen.py runs inside make check and:

  1. Reflects the live exported name set + call signatures of every SDK module.
  2. Diffs it against the committed snapshot backend/vectis_sdk/api_surface.json.
  3. Fails on any removal of an exported name or any narrowing of a signature (dropping a parameter, making an optional parameter required). Additions always pass.

When you intentionally add to the surface, regenerate the snapshot:

make freeze-sdk        # rewrites vectis_sdk/api_surface.json from the live surface

Review the diff — a removal or narrowing showing up in the snapshot is a signal you are breaking the contract and owe a major version bump.

vectis_testkit — fixtures + contract suites

vectis_testkit is core-side test infrastructure. Unlike an extension, it may (and does) import vectis.core.* / vectis.modules.* directly — its fixtures.py was extracted verbatim from backend/tests/conftest.py and is now the single source of truth for the fixture graph. The in-tree suite and every out-of-tree extension get the identical fixtures.

An extension author does not import vectis_testkit by name. They reach the same surface two ways:

  • Fixtures arrive through the auto-registered pytest11 plugin — no conftest wiring needed once the package is installed.
  • Contract helpers arrive through the frozen facade vectis_sdk.testing.

Fixtures (the pytest11 plugin)

vectis_testkit.plugin is registered as a pytest11 entry point in the vectis distribution, so pip install makes the fixtures live with no conftest. Importing the plugin imports vectis_testkit.env first, which pins the test environment (DATABASE_URLvectis_test, secrets) before any vectis.* import fires — the ordering that makes a real-PostgreSQL test run reproducible.

Fixtures provided include app, client, db_session, event_bus, strategy_resolver, permission_registry, rbac_engine, custom_field_registry, employee_context, staff_context, sample_variant, and seed_commerce_basics.

# In an extension's test — no conftest, no imports; the plugin supplies these.
async def test_my_strategy_registers(strategy_resolver, seed_commerce_basics):
    ...

The five contract suites

Each suite is one assert_* helper (from vectis_sdk.testing) an extension calls to prove a slice of platform compliance. Ground truth lives in backend/vectis_testkit/contracts/.

# Helper Contract Source
1 assert_imports_clean The extension imports core only via vectis_sdk.* (the import ratchet, reusable). The AST import-scan is re-implemented in-package so an out-of-tree wheel need not ship scripts/; a parity test pins it to the gate script. contracts/imports.py
2 assert_manifest_valid The manifest is a well-formed ExtensionManifest. contracts/manifest.py
3 assert_lifecycle_clean activate → uninstall leaves no stale ledger entry (the lifecycle property). contracts/lifecycle.py
4 assert_strategy_satisfies / assert_webhook_vectors A strategy impl satisfies its ABC; a PaymentWebhookHandler matches its golden WebhookVector cases. contracts/strategy.py
5 assert_schema_composes The extension's GraphQL mixins compose into the schema cleanly. contracts/schema.py
from vectis_sdk.testing import assert_imports_clean, assert_manifest_valid

def test_extension_is_compliant():
    assert_manifest_valid(my_manifest)
    assert_imports_clean(ext_root="ext_myprovider")  # path to the extension package dir

Building the packages

Both backend/vectis_sdk/ and backend/vectis_testkit/ carry their own pyproject.toml ([project] metadata, Apache-2.0 license, version, the optional [engine] extra) and a py.typed marker, and are listed in the hatch wheel packages list so vectis_testkit ships and its pytest11 entry point resolves out-of-tree.

cd backend
python -m build vectis_sdk       # the two open wheels
python -m build vectis_testkit

For the full pre-publish dry run (version-cascade check → build → twine check → SBOM) use make release-check; publishing itself runs from the dormant release.yml workflow. Both are documented in Extension Packaging & Extraction.

Public wheels resolve without the engine

Because the facade is lazy and the engine is an optional [engine] extra, the public vectis-sdk / vectis-testkit wheels install and import on public PyPI with no engine present — useful for contract-testing an extension against the interface. For the parts that need core at runtime (a resolved SDK symbol, vectis_testkit's seeded-DB fixtures) install the [engine] extra where the licensed core is available. vectis_testkit's contracts/* are core-free (they re-implement the import-scan AST walk in-package), so the contract helpers work before the engine is installed.