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 fromvectis_sdk.*and nothing else; the extension→core import allowlist is{}.vectis_testkit— reusable pytest fixtures (apytest11plugin) 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
- Authoring contract (manifests, contribution surfaces, migrations): Building Extensions.
- Third-party / commercial extension authoring: Building Extensions (External Developers).
- Admin-panel UI contributions: Admin Contributions.
- Per-extension usage/config docs: Extensions section.
- General backend testing: Testing.
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, neverimport vectis_sdkexpecting the surface to be attached. models,catalog, andservicesare the exceptions — they are eager: resolving them builds the full ORM mapper closure, so they require the engine.vectis_sdk.strategieslikewise 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:
- Reflects the live exported name set + call signatures of every SDK module.
- Diffs it against the committed snapshot
backend/vectis_sdk/api_surface.json. - 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:
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
pytest11plugin — noconftestwiring 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_URL → vectis_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.
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.