Plugin setup
Install @vendurepos/plugin, the settings it needs, and what it adds to your store.
Check your store
Use Vendure ^3.6.0 (tested on 3.7.3), Postgres and Node ^20.19 || >=22.12.
MySQL, MariaDB and SQLite are not supported. You need @vendure/email-plugin only if you send email.
Install the version you need
Version 0.1.0 is on npm and is enough to sell and run the register:
npm install @vendurepos/plugin@0.1.0Version 0.2.0 adds device keys for tills: the New POS till key button and TallyPosSell permission.
Version 0.3.0 lets the plugin record fees, shipping and custom lines; the till does not add them today. Both are tagged in the repo but are not yet on npm.
Until the npm release, install 0.3.0 from the repo:
git clone --branch plugin-v0.3.0 --depth 1 https://github.com/vendurepos/app.git vendurepos-app
cd vendurepos-app/packages/vendure-plugin
npm ci
npm run build
npm packThen, in your Vendure project:
npm install /path/to/vendurepos-app/packages/vendure-plugin/vendurepos-plugin-0.3.0.tgzConfigure the plugin and migrations
Follow the quick start for the plugin in plugins, the migration,
'bearer' in authOptions.tokenMethod, the POS origin in CORS and the order tax strategy.
TallyPosPlugin has no .init() options. For device keys, also enable 'api-key'; see Connect a till.
Generate a migration for your store as in the quick start, or register the bundled migrations; never use both for the same changes.
As of 0.3.0 there are four bundled migrations; TallyPosV51790900000000 ships in 0.3.0:
import {
TallyPosPlugin, TallyPos1790648006022, TallyPosVp2a1790720000000, TallyPosRegister1790800000000,
TallyPosV51790900000000,
} from '@vendurepos/plugin';Inside your Vendure config, register them in dbConnectionOptions with synchronize: false:
dbConnectionOptions: {
// …your existing connection settings
synchronize: false,
migrations: [TallyPos1790648006022, TallyPosVp2a1790720000000, TallyPosRegister1790800000000, TallyPosV51790900000000, /* your own migrations */],
},Run them before starting Vendure:
runMigrations(config);They create the tally_command ledger, read-only Order and OrderLine custom fields, register tables and indexes.
Set up email
POS orders send no order-confirmation email. If you use @vendure/email-plugin, replace orderConfirmationHandler with tallyOrderConfirmationHandler:
import { EmailPlugin, defaultEmailHandlers, orderConfirmationHandler } from '@vendure/email-plugin';
import { tallyOrderConfirmationHandler } from '@vendurepos/plugin/email';
EmailPlugin.init({
handlers: defaultEmailHandlers.map(handler =>
handler === orderConfirmationHandler ? tallyOrderConfirmationHandler : handler),
// …
});The main entry @vendurepos/plugin never loads @vendure/email-plugin.
Check what your channels receive
On server start, every channel that lacks them gets the tally-pos payment method, the tally-in-store shipping method and a walk-in customer. Both methods are closed to the Shop API. A channel created later gets them at the next server start.
Each method is created once in the default channel and assigned to the others, so the default channel lists one of each.
Vendure refuses deletion of tally-pos in the default channel unless forced; a forced delete removes it from every channel. Deleting it in another channel removes it from that channel only. Deleting tally-in-store in any channel removes it from every channel.
A channel missing either method refuses new sales (store_configuration) until the next server start puts it back.
Give staff TallyPosSell (0.2.0+) or Vendure's broader CreateOrder; the plugin's routes accept either.
From 0.3.0 the plugin records one shipping charge per order. Fees are Vendure surcharges (TALLY-FEE); custom lines are order lines on a disabled "POS custom item" product (SKU TALLY-CUSTOM-ITEM). A fee with no tax class uses the store's default tax category: mark one as default in Settings → Tax categories.
Add a customer index when needed
If your store has more than about 50,000 customers, or POS sales with an email are visibly slow, run this on its own in psql, outside a migration: CONCURRENTLY must run outside a transaction.
CREATE INDEX CONCURRENTLY "IDX_customer_email_lower" ON "customer" (lower("emailAddress")) WHERE "deletedAt" IS NULL;If it fails part-way, it leaves an INVALID index: drop it and run it again. The server logs a warning at start when customer has more than 50,000 rows and no such index.
Check your own code
Each sale runs in one database transaction. Custom order-process hooks, blocking event handlers and custom strategies inside it must not send email, call payment providers or webhooks, or write to other databases: a rolled-back sale would keep that side effect.
Read the full plugin reference.