The Modern Foundation for Commerce
This project is under active development and is not ready for production use. Please proceed with caution. Use at your own risk.
Documentation: 86d.app/docs/modules/multi-currency
@86d-app/multi-currency
Multi-currency support for 86d commerce stores. Manage currencies, exchange rates, price conversions, and per-product price overrides.
Installation
The module is included in the 86d monorepo. Enable it in your store's template config.json:
{
"modules": ["multi-currency"]
}
Usage
import multiCurrency from "@86d-app/multi-currency";
// Register with options
multiCurrency({
baseCurrency: "USD",
});
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
baseCurrency |
string |
"USD" |
Default base currency ISO 4217 code |
Store Endpoints
| Endpoint | Method | Description |
|---|---|---|
/currencies |
GET | List all active currencies |
/currencies/convert |
POST | Convert an amount between currencies |
/currencies/product-price |
POST | Get a product's price in a specific currency |
Convert Price
POST /currencies/convert
{
"amount": 100,
"to": "EUR",
"from": "USD" // optional, defaults to base currency
}
// Response: { "amount": 85, "formatted": "85,00EUR", "currency": "EUR" }
Get Product Price
POST /currencies/product-price
{
"productId": "prod-123",
"basePriceInCents": 9999,
"currencyCode": "EUR"
}
// Response: { "amount": 8499, "formatted": "84,99EUR", "currency": "EUR" }
Admin Endpoints
| Endpoint | Method | Description |
|---|---|---|
/admin/currencies |
GET | List all currencies |
/admin/currencies/create |
POST | Create a new currency |
/admin/currencies/:id |
GET | Get currency by ID |
/admin/currencies/:id/update |
POST | Update a currency |
/admin/currencies/:id/delete |
POST | Delete a currency |
/admin/currencies/:id/set-base |
POST | Set as base currency |
/admin/currencies/update-rate |
POST | Update a single exchange rate |
/admin/currencies/bulk-update-rates |
POST | Update multiple exchange rates |
/admin/currencies/rate-history |
POST | Get exchange rate history |
/admin/currencies/price-override |
POST | Set a product price override |
/admin/currencies/price-overrides/:productId |
GET | List overrides for a product |
/admin/currencies/price-overrides/:id/delete |
POST | Delete a price override |
Store Components
The module exports customer-facing components for MDX templates:
CurrencySelector
Dropdown to switch the active currency. Hidden when only one currency is active.
<CurrencySelector value={selectedCurrency} onChange={setCurrency} />
<CurrencySelector compact />
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
— | Currently selected currency code (ISO 4217) |
onChange |
(code: string) => void |
— | Called when user selects a different currency |
compact |
boolean |
false |
Show code only (no full name) |
PriceDisplay
Show a price converted to the selected currency, with optional compare-at (sale) display.
<PriceDisplay productId="prod-123" basePriceInCents={9999} currencyCode="EUR" />
| Prop | Type | Default | Description |
|---|---|---|---|
productId |
string |
— | Product ID (used for price override lookup) |
basePriceInCents |
number |
required | Base price in smallest currency unit |
currencyCode |
string |
— | Target currency code |
compareAtPriceInCents |
number |
— | Compare-at price for sale display |
className |
string |
— | Additional CSS class |
Admin UI
The module includes admin UI components in src/admin/components/index.tsx (client components using useModuleClient):
| Page | Component | Description |
|---|---|---|
/admin/currencies |
CurrencyList |
Currency list with exchange rates and status indicators |
/admin/currencies/new |
CurrencyForm |
Create a new currency |
/admin/currencies/:id |
CurrencyDetail |
Currency detail with rate history and price overrides |
/admin/currencies/:id/edit |
CurrencyForm |
Edit an existing currency |
Controller API
Access via ctx.context.controllers.multiCurrency:
Currency Management
create(params)— Create a currency (code is auto-uppercased)getById(id)/getByCode(code)— Lookupupdate(id, params)— Update currency detailsdelete(id)— Delete (cannot delete base currency)list({ activeOnly? })— List currencies sorted bysortOrder
Base Currency
getBaseCurrency()— Get the store's base currencysetBaseCurrency(id)— Set a new base currency (unsets previous)
Exchange Rates
updateRate({ currencyCode, rate, source? })— Update rate and record historybulkUpdateRates(rates[])— Update multiple rates at oncegetRateHistory({ currencyCode, limit? })— Get historical rates
Price Conversion
convert({ amount, to, from? })— Convert between currenciesformatPrice(amount, currencyCode)— Format with locale settingsgetProductPrice({ productId, basePriceInCents, currencyCode })— Resolve best price
Price Overrides
setPriceOverride({ productId, currencyCode, price, compareAtPrice? })— Set fixed pricegetPriceOverride(productId, currencyCode)— Get overridelistPriceOverrides(productId)— List all overrides for a productdeletePriceOverride(id)— Remove an override
Types
interface Currency {
id: string;
code: string; // ISO 4217 (e.g., "USD")
name: string; // Display name
symbol: string; // Currency symbol
decimalPlaces: number; // 0 for JPY, 2 for USD
exchangeRate: number; // Relative to base (base = 1)
isBase: boolean;
isActive: boolean;
symbolPosition: "before" | "after";
thousandsSeparator: string;
decimalSeparator: string;
roundingMode: "round" | "ceil" | "floor";
sortOrder: number;
}
interface PriceOverride {
id: string;
productId: string;
currencyCode: string;
price: number; // In smallest unit (cents)
compareAtPrice?: number;
}
interface ConvertedPrice {
amount: number;
currency: Currency;
formatted: string;
}
Events
| Event | Payload | Description |
|---|---|---|
currency.created |
{ id, code } |
New currency added |
currency.updated |
{ id, code } |
Currency details changed |
currency.deleted |
{ id, code } |
Currency removed |
currency.baseChanged |
{ id, code } |
Base currency switched |
currency.rateUpdated |
{ code, oldRate, newRate, source } |
Exchange rate changed |
Notes
- Only one base currency per store. Base rate is always 1.
- Cross-currency conversion goes through the base rate (EUR -> USD -> GBP).
- Price overrides take priority over automatic conversion.
- Zero-decimal currencies (JPY, KRW) are fully supported via
decimalPlaces: 0. - Exchange rate history is recorded automatically on every rate update.