npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ejosterberg/vendure-plugin-opensalestax

v1.4.0

Published

Vendure plugin for destination-based US sales tax via the self-hosted OpenSalesTax engine.

Readme

@ejosterberg/vendure-plugin-opensalestax

ci npm License: Apache-2.0 OR GPL-2.0-or-later

Vendure plugin for destination-based US sales tax via the self-hosted OpenSalesTax engine. No SaaS, no per-transaction fees, no third-party API keys.

  • Implements Vendure's TaxLineCalculationStrategy — Vendure invokes us per OrderLine; we return per-jurisdiction tax lines (state, county, city, transit district, etc.)
  • USD-only / US-only. Non-USD or non-US orders pass through to Vendure's built-in TaxRate pipeline unchanged.
  • Fail-soft default. If the OpenSalesTax engine is unreachable, the plugin returns zero tax + logs a warning; Vendure's default tax rates take over so checkouts don't block. Opt into fail-hard via env var.
  • No inbound HTTP surface. Pure outbound HTTP to the configured engine URL. The plugin runs in-process inside your Vendure server.

Compatibility

| Plugin | Vendure | OST engine | Node | |--------|---------|-----------|------| | 0.1.x | 3.x | 0.22+ (v1 API) | 20+ |

Quickstart (≤10 minutes)

1. Install

npm install @ejosterberg/vendure-plugin-opensalestax

2. Wire it into vendure-config.ts

import { OpenSalesTaxPlugin } from '@ejosterberg/vendure-plugin-opensalestax';
import { VendureConfig } from '@vendure/core';

export const config: VendureConfig = {
  // ...your existing config...
  plugins: [
    // ...your existing plugins...
    OpenSalesTaxPlugin.init({
      apiUrl: process.env.OSTAX_API_URL!,
      // failHard: true,        // optional; default fail-soft
      // apiToken: '...',       // optional; X-API-Key header
      // timeoutMs: 5000,       // optional; per-request timeout
    }),
  ],
};

3. Set the environment variable

# Point at your OpenSalesTax engine instance (HTTP or HTTPS)
export OSTAX_API_URL="https://ost.your-domain.com"

# Optional:
# export OSTAX_API_TOKEN="..."
# export OSTAX_FAIL_HARD=1
# export OSTAX_TIMEOUT_MS=5000

4. (Recommended) Create a US Zone in Vendure Admin

As of v1.1.0 this step is optional — the plugin's OstaxTaxZoneStrategy auto-resolves the active zone for US shipping addresses, so checkout works even with a fresh Vendure install. Still, having a real Zone is best practice.

In the Admin UI:

  1. Settings → Zones → Add a Zone with United States as a member country (name it whatever you like).
  2. Settings → Tax Categories → Confirm "Standard" exists (or create it).
  3. Settings → Tax Rates → Create a placeholder rate "US Standard" of 0% in your US zone, category Standard. (OST provides the real rate via the strategy; this placeholder exists so Vendure's normal pipeline always has something to fall back to.)

If you skip this step, the plugin will log a one-time WARN at startup recommending you create a US Zone, and checkout will fall through to your channel's default Zone — taxes still flow from the OST engine either way.

5. Restart Vendure

On boot you'll see a log line like:

[OpenSalesTaxPlugin] OpenSalesTax engine reachable: status=ok version=0.55.4 db=true

If the engine is unreachable, you'll see a WARN (default fail-soft mode) — the plugin still loads and Vendure's default tax pipeline takes over until the engine comes back.

6. Place a test US order

Use the Shop GraphQL API to add an item, set a US shipping address, and inspect taxLines:

mutation {
  setOrderShippingAddress(input: {
    streetLine1: "100 N 6th St"
    city: "Minneapolis"
    province: "MN"
    postalCode: "55403"
    countryCode: "US"
  }) {
    ... on Order {
      lines {
        taxLines { description rate }
      }
      totalWithTax
    }
  }
}

You should see one TaxLine per OST jurisdiction (e.g. "Minnesota (State)", "Minneapolis (City)", "Hennepin Transit (Transit)") with the correct percentage rates. Vendure multiplies these against the line price to compute the actual tax amount.

Configuration reference

| Option | Env var | Default | Description | |---------------|-----------------------|---------|-------------| | apiUrl | OSTAX_API_URL | — | Required. Base URL of your OST engine. Must be http: or https:. Validated at plugin init. | | apiToken | OSTAX_API_TOKEN | — | Optional. Sent as the X-API-Key header. Most self-hosted deployments don't need this. | | failHard | OSTAX_FAIL_HARD=1 | false | When true, engine errors throw (Vendure surfaces as order error). Default false returns [] + warns. | | timeoutMs | OSTAX_TIMEOUT_MS | 5000 | Per-request timeout in milliseconds. | | categoryByTaxCategoryName | — | {} | Map Vendure TaxCategory.name → OST category. See "Tax category mapping" below. | | defaultCategory | — | 'general' | OST category for lines whose TaxCategory name doesn't appear in the map. Set to '' (empty string) to make unmapped lines non-taxable. | | enabledStates | — | — | Allowlist of US state codes (uppercase 2-letter). When set, the plugin computes only for orders shipping to one of these. See "Per-state nexus filter" below. | | disabledStates | — | — | Denylist of US state codes. Mutually exclusive with enabledStates. |

Options passed to init() take priority over env vars.

Per-state nexus filter (v1.2.0+)

Sales-tax nexus is the legal threshold (physical presence, economic activity) that obligates a merchant to collect and remit tax in a state. No US merchant has nexus in all 50 states, and the OpenSalesTax engine doesn't decide nexus for you — that's your call (or your accountant's). Use one of these options to tell the plugin where you collect:

// Pattern 1 — allowlist: most small merchants
OpenSalesTaxPlugin.init({
  apiUrl: process.env.OSTAX_API_URL!,
  enabledStates: ['MN', 'WI', 'IA'],   // collect only in these 3
}),

// Pattern 2 — denylist: larger merchants with broad footprints
OpenSalesTaxPlugin.init({
  apiUrl: process.env.OSTAX_API_URL!,
  disabledStates: ['MT', 'WY'],   // skip these; collect everywhere else
}),

When the order's shippingAddress.province doesn't match the configured filter, the strategy returns [] and Vendure's default TaxRate pipeline takes over (typically zero tax for a US-only merchant).

Validation: state codes must be uppercase 2-letter ISO 3166-2 subdivision codes (the same format Vendure's stock checkout form populates). Invalid entries (lowercase, full names, non-letter) throw at plugin init with a list of bad codes. Setting both enabledStates and disabledStates throws.

Empty array (enabledStates: []) is treated as no filter (footgun mitigation). To disable the plugin entirely, remove it from your plugins array.

Migration from v1.1: zero config required. v1.1 computed for every US ZIP (no nexus filtering); v1.2's defaults preserve that. Add the filter when you're ready.

Tax category mapping (v1.1.0+)

The OST engine accepts a category per line item and applies category-specific rules — clothing exemptions in some states, groceries usually exempt, prescription drugs always exempt, etc. Map each of your Vendure TaxCategory records to one of OST's six categories so the engine can apply the right rules.

OST categories (the values in the map):

| Value | Use for | |-------|---------| | 'general' | General merchandise (default) | | 'clothing' | Apparel (PA, MN, NJ exempt; rules vary) | | 'groceries' | Unprepared food (most states exempt or reduced) | | 'prescription_drugs' | Rx drugs (universally exempt) | | 'prepared_food' | Restaurant / hot food (often higher rate) | | 'digital_goods' | Digital downloads (varies by state) | | '' (empty string) | Skip the line entirely — non-taxable |

Worked example — a mixed retailer:

In the Vendure Admin UI, create a TaxCategory for each product class you sell ("Clothing", "Food", "Digital", "Gift Cards", and the existing "Standard"). Assign each ProductVariant to the right category as you create it.

Then in vendure-config.ts:

OpenSalesTaxPlugin.init({
  apiUrl: process.env.OSTAX_API_URL!,
  categoryByTaxCategoryName: {
    'Clothing':       'clothing',
    'Food':           'groceries',
    'Digital':        'digital_goods',
    'Gift Cards':     '',           // non-taxable; engine never called
    // 'Standard' isn't listed → falls back to defaultCategory
  },
  defaultCategory: 'general',
}),

A merchant whose entire catalog is clothing can skip the mapping and just set defaultCategory: 'clothing'.

Validation: invalid values throw at plugin init — you'll see the error at server boot, not at first checkout. Valid values are listed in the table above.

Migration from v1.0: zero config required. v1.0 hardcoded category: 'general' for every line; v1.1's defaults reproduce that behavior exactly. Add the mapping when you're ready.

How it works

┌─────────────────────────────────────────────────┐
│ Merchant's Vendure server                       │
│  ┌─────────────────────────────────────────┐    │
│  │ Order checkout flow                     │    │
│  │  ↓                                      │    │
│  │ TaxZoneStrategy.determineTaxZone()      │    │
│  │  ↓ (US ship-to → US Zone, else default) │    │
│  │ TaxLineCalculationStrategy.calculate()  │    │
│  │  ↓                                      │    │
│  │ OstaxTaxLineStrategy                    │    │
│  │  ├─ Gate: USD? US? valid ZIP?           │    │
│  │  └─ POST /v1/calculate ─────────────────┼────┼──→ OpenSalesTax engine
│  └─────────────────────────────────────────┘    │   (merchant-self-hosted)
└─────────────────────────────────────────────────┘

The plugin registers two strategies:

  • OstaxTaxZoneStrategy (added in v1.1.0): inspects the order's shipping country code; if it's US, finds a Zone whose members include the United States and returns it; if no US Zone exists, returns the channel's default Zone (with a one-time WARN suggesting you create one). For non-US orders, returns the channel default unchanged.
  • OstaxTaxLineStrategy: per-line tax calculation against the OST engine, gated on USD currency + US shipping country
    • valid ZIP regex.

For each order line, the plugin:

  1. Gates on currencyCode === "USD", shippingAddress.countryCode === "US", and ZIP regex ^\d{5}(-\d{4})?$. If any fail → return [] and Vendure's built-in TaxRate pipeline takes over.
  2. Sends a single-line POST /v1/calculate to the engine with the line's proratedUnitPrice (in dollars) and category "general".
  3. Maps each jurisdiction in the response to a Vendure TaxLine (description + taxRate). Vendure handles the multiplication.

Troubleshooting

"OpenSalesTax engine health check failed at startup"

The plugin couldn't reach apiUrl at boot. The plugin still loads (fail-soft); calculations will return zero tax until the engine is reachable. Check:

  • Is OSTAX_API_URL set and reachable from the Vendure host?
  • Is the OST engine container running and healthy?
  • Can you curl $OSTAX_API_URL/v1/health from the Vendure server?

"Returns zero tax even for US orders"

Possible causes:

  1. The engine is unreachable — check Vendure boot logs for the health-check warning.
  2. The order's shippingAddress.countryCode is not exactly "US" (case-sensitive).
  3. The order's postalCode doesn't match ^\d{5}(-\d{4})?$.
  4. The order's currencyCode is not "USD".
  5. The OST engine has no rate data for the destination ZIP (rare; check engine logs).

"I see a one-time WARN about no US Zone"

Expected if you haven't created a Zone with United States as a member. Checkout still works — the plugin falls through to your channel's default Zone. To silence the WARN, create a US Zone in Settings → Zones. The WARN is rate-limited to once per process so it won't flood your logs.

"I want errors to surface, not silently fall back"

Set OSTAX_FAIL_HARD=1 (or init({ failHard: true })). Engine errors then throw, which Vendure surfaces as an order error in the Admin UI.

"Plugin doesn't appear to register"

Verify in the running Vendure server:

import { ConfigService } from '@vendure/core';

// In a NestJS-aware context (e.g. an admin-API resolver):
const cfg = injector.get(ConfigService);
console.log(cfg.taxOptions.taxLineCalculationStrategy);
// Expect: OstaxTaxLineStrategy

Common causes: forgot to call OpenSalesTaxPlugin.init({...}) (the init() is required to capture options); plugin registered after a different plugin that also overrides taxLineCalculationStrategy.

What this plugin does NOT do

By design (constitution §6 + §10):

  • Filing or remittance. It computes tax. You file.
  • Address validation. Bring your own / use Vendure's.
  • Non-USD currencies. Returns [] so Vendure's default pipeline applies.
  • Non-US jurisdictions. Same — returns [].
  • Tax-exempt customer certificate validation.
  • Marketplace facilitator (NJ / CA seller-of-record edge cases).
  • Inbound webhooks / standalone server. Pure outbound HTTP to your engine.

For any of these, run a separate process or use a dedicated service.

Security

The plugin exposes no inbound HTTP routes, no GraphQL resolvers, no webhook receivers. The trust boundary is your Vendure host; whatever code loaded the plugin is already trusted.

apiUrl is validated at plugin init: URL parse + scheme allowlist (http:, https:). Customer addresses, line item descriptions, product names, and customer email are never logged.

Reporting vulnerabilities: see SECURITY.md.

Contributing

DCO sign-off mandatory on every commit (git commit -s). See CONTRIBUTING.md. Dual-licensed under your choice of Apache-2.0 OR GPL-2.0-or-later. See LICENSE.

Related projects

| Connector | Stack | Repo | |-----------|-------|------| | OpenSalesTax engine | Python | opensalestax | | Medusa v2 | TypeScript | medusa-plugin-opensalestax | | WooCommerce | PHP | woocommerce-opensalestax | | Odoo | Python | opensalestax-odoo-src | | Vendure | TypeScript | this repo |