Architecture
The port keeps the architecture of laravel-sisp 2.x: actions, builders, pipelines, drivers, and contracts. Laravel’s container is replaced by explicit constructor wiring in createSisp. The layout is hexagonal: core contracts and domain sit at the centre; application orchestrates them; infrastructure provides the adapters; presentation exposes the HTTP surface.
src/
core/
contracts/ SispDriver, CredentialsResolver, PaymentPipe, CallbackPipe, SispStorage (port)
domain/
enums/ statuses, transaction codes, message types, translations
errors/ SispError and typed subclasses
policies/ domain rules (retry eligibility, refund limits, ...)
value-objects/ CallbackPayload, PaymentRequest, RefundRequest, credentials
application/
config.ts SispConfig, defaults, credential mapping
create-sisp.ts composition root
sisp.ts public facade
scoped-sisp.ts forCredentials facade
wiring.ts credential-scoped service wiring (shared with ScopedSisp)
events.ts typed emitter
sandbox.ts fake gateway payload builder
actions/ one unit of work each, ported 1:1 from the PHP actions
builders/ PaymentBuilder, RefundBuilder
pipelines/
payment/ context plus default payment pipes
callback/ context plus default callback pipes
infrastructure/
drivers/ SispManager, production, sandbox, TransactionStatusClient
fingerprints/ token, payment, callback, refund algorithms
http/ pure handlers, idempotency resolver, validation, results, auto-submit forms
storage/
column-codec.ts how each adapter reads and writes an encrypted column
rekey.ts the per-row rekey decision shared by the three adapters
reencrypt-batch.ts the page walk and the counters behind reencryptBatch
knex/ KnexStorage, the default built from the database config
models/ repository implementations (Transaction, TransactionAttempt, PaymentIntent, ...)
migrations/ bundled schema, mirror of the Laravel migrations
create-knex.ts knex instance factory
auto-migrate.ts migration runner
encryption.ts AES-256-GCM payload cipher
locking.ts row-level lock helper for supported drivers
log-context.ts AsyncLocalStorage log source
records.ts raw DB record types
prisma/ PrismaStorage, at the ./prisma subpath
drizzle/ DrizzleStorage, at the ./drizzle subpath
presentation/
cli/ sisp binary (migrate, reconcile-pending, prisma, prune-metadata, rotate-key)
express/ thin Express adapter
fastify/ thin Fastify adapter
nest/ thin NestJS adapter
support/ SispAmount, generators, countries, signed URLs, user agent
Key decisions
- Pipelines are arrays of
{ handle(context, next) }objects executed by a tiny async runner. The default pipe sets can be customized per flow throughpipelines.paymentandpipelines.callback. - Models are the knex adapter’s repository implementations behind the
SispStorageport.Transaction.updatediffs changes, encrypts the payload, and appends the audit log in one place. - Payment intents live at the HTTP boundary. They reserve checkout keys, link keys to transactions, and allow safe replay of the same checkout.
- Transaction attempts live under the parent transaction. They preserve every gateway submission and let callbacks update the exact attempt that SISP answered.
- Drivers decide the payment endpoint and the status API client.
manager.extend('custom', factory)registers new gateways. - Credential scoping rebuilds only the credential-dependent services (
wiring.ts) around a static resolver, which is howforCredentialsworks without a container. - Parity with the PHP package is pinned by golden vectors generated from the real implementation, not by re-derived constants.
Storage adapters
The persistence layer sits behind an ORM-neutral port, SispStorage, defined in src/core/contracts/storage.ts: ten repositories plus a transaction() unit-of-work, an optional migrate?(), and destroy(). The port leaks no engine types.
Three adapters ship with the package:
KnexStorage(src/infrastructure/storage/knex/) - the default, built from thedatabaseconfig. Handles migrations automatically. The main entry loads it through a dynamic import taken only when nostorageis injected, so a consumer that brings its own adapter never evaluates the adapter or theknexpackage. In the ESM build the adapter is a separate chunk the entry never imports statically; the CJS build ships it inline indist/index.cjs, still behind the same lazy branch. ImportknexOffrom@akira-io/sisp/knexfor the typed instance and raw queries.PrismaStorage(src/infrastructure/storage/prisma/) - available at the@akira-io/sisp/prismasubpath. Injected viacreateSisp({ storage: createPrismaStorage(prisma, tables, appKey, { provider }) }). The core bundle never imports@prisma/client.DrizzleStorage(src/infrastructure/storage/drizzle/) - available at the@akira-io/sisp/drizzlesubpath. Injected viacreateSisp({ storage: createDrizzleStorage(db, tables, appKey, { dialect }) }). The core bundle never importsdrizzle-orm. Its schema definitions, its DDL and its repositories all read one canonical table specification, so the three dialects cannot drift apart.
All three adapters are validated by the shared contract suite tests/storage/contract.ts, which guarantees behavioral parity. See Storage Adapters for the Prisma quick start and instructions for implementing custom adapters.
The application layer runs every database transaction through storage.transaction(tx => ...) with locked reads via the repository ...ForUpdate methods, so atomicity and locking are adapter-decided.
Knex-coupled surfaces live at the @akira-io/sisp/knex subpath: knexOf, createKnexInstance, runMigrations, MIGRATIONS_TABLE, PayloadCipher, runWithLogSource. The main entry’s database.connection config type is package-owned (string | Record<string, unknown>), not knex’s, and Sisp exposes no raw driver handle at all. The CLI migrate command still talks to knex directly, since it is a binary and not part of the package’s type surface. Engine selection and genericizing the database config are a later phase.
Differences from the Laravel package
- Rendering is out of scope: the core returns render-ready data and HTML auto-submit forms only.
- PDF invoice generation is deferred; the invoice rows and statuses are maintained.
- Geolocation providers are not bundled; metadata captures device data only.
- Scheduling is the host’s job: call
reconcilePending()from your scheduler or cron the CLI.
Next: Index