Shipping Configuration Guide
Setup and manage shipping rates, carriers, and address validation for the e-commerce platform
Shipping Configuration Guide
How to set up and manage shipping for the Jose Madrid Salsa e-commerce platform.
Table of Contents
- Overview
- Architecture
- Environment Variables
- Admin Settings
- Carrier API Setup
- Rate Calculation Logic
- Fallback Rate System
- PO Box Handling
- Caching
- Address Validation
- Key Files
Overview
The shipping system calculates real-time rates from carrier APIs (EasyPost or Shippo) with automatic fallback to estimate-based rates when the API is unavailable. This ensures checkout is never blocked by shipping failures.
Design Principles
- Never block checkout - All errors fall back to estimate rates
- Server-side truth - Shipping cost is always recalculated server-side; client values are never trusted
- Configurable - Free shipping threshold and origin address are managed from the admin panel
- PO Box aware - Automatically filters to USPS-only options for PO Box addresses
Architecture
Checkout Page Server
| |
| POST /api/checkout/ |
| calculate-shipping |
|------------------------------>|
| |
| +----------+-----------+
| | Check in-memory |
| | cache (5 min TTL) |
| +----------+-----------+
| |
| +----------+-----------+
| | Fetch product |
| | weights from DB |
| +----------+-----------+
| |
| +----------+-----------+
| | calculateShipping() |
| | |
| | 1. Free threshold? |
| | 2. Carrier API call |
| | 3. Fallback rates |
| +----------+-----------+
| |
| { availableOptions, cost } |
|<------------------------------|Environment Variables
Required
| Variable | Description | Example |
|---|---|---|
SHIPPING_API_KEY | API key for the shipping provider (EasyPost or Shippo) | EZAKxxxxxxxx |
Optional
| Variable | Default | Description |
|---|---|---|
SHIPPING_PROVIDER | easypost | Shipping provider to use (easypost or shippo) |
SHIPPING_TEST_MODE | false | Set to true to use test/sandbox mode |
SHIPPING_ORIGIN_ADDRESS | 123 Main St | Fallback origin street address |
SHIPPING_ORIGIN_CITY | San Francisco | Fallback origin city |
SHIPPING_ORIGIN_STATE | CA | Fallback origin state |
SHIPPING_ORIGIN_ZIP | 94111 | Fallback origin ZIP code |
The SHIPPING_ORIGIN_* environment variables serve as defaults. The admin settings page origin address takes precedence when configured.
Admin Settings
URL: /admin/settings/shipping
Required permission: settings:read to view, settings:write to modify.
Configurable Fields
Free Shipping Threshold
- Numeric dollar amount (e.g.,
75.00) - Orders at or above this amount receive free shipping
- Leave blank to disable free shipping
- Stored in the
ShippingSettingstable (singleton row) - Default fallback:
$50.00(hardcoded inSHIPPING_RATES.FREE_SHIPPING_THRESHOLD)
Origin Address
- Street address, city, state, ZIP code, country
- Used as the "from" address for carrier rate API calls
- Required for accurate rate calculations
- Admin page shows warning badge if not configured
Default Carrier
- Dropdown: None (use cheapest), USPS, UPS, FedEx
- When set to "None", the system returns the cheapest available rate as the default option
Enabled Carriers
- Checkbox selection: USPS, UPS, FedEx
- Controls which carriers appear in rate calculations
- Admin page shows warning if no carriers are enabled
Status Dashboard
The admin page displays current configuration status with badges:
- Free shipping: Active (with threshold) or Disabled
- Origin address: Configured (with address preview) or Required
- Carriers: Active (count + names) or Warning (none enabled)
All settings changes are audit-logged with the user ID, action, and changed values.
Carrier API Setup
Supported Providers
EasyPost
- Create an account at easypost.com
- Obtain your API key from the dashboard
- Set
SHIPPING_API_KEYto your EasyPost API key - Set
SHIPPING_PROVIDER=easypost(this is the default) - For testing, set
SHIPPING_TEST_MODE=true
Current status: The EasyPost client (EasyPostClient in lib/shipping-api.ts) returns mock rates. Install @easypost/api and implement real API calls for production.
Shippo
- Create an account at goshippo.com
- Obtain your API token from the dashboard
- Set
SHIPPING_API_KEYto your Shippo API token - Set
SHIPPING_PROVIDER=shippo - For testing, set
SHIPPING_TEST_MODE=true
Current status: The Shippo client (ShippoClient in lib/shipping-api.ts) returns mock rates. Install the shippo package and implement real API calls for production.
Validation
Call validateShippingConfiguration() from lib/shipping-api.ts at app startup to verify:
- API key is set
- Provider client can be instantiated
- Test rate fetch returns results
const result = await validateShippingConfiguration()
// { configured: true, provider: 'easypost', testMode: false }
// { configured: false, error: 'SHIPPING_API_KEY is not set' }Rate Calculation Logic
calculateShipping() in lib/shipping-calculator.ts
The main entry point for all shipping calculations. Called by both the checkout API and the shipping estimate endpoint.
Step-by-Step Flow
- Free shipping check - Fetch threshold from DB (
ShippingSettingssingleton); if subtotal >= threshold, return$0.00immediately - International check - Non-US addresses use estimate-based rates (carrier API not yet supported for international)
- Parcel dimensions - Aggregate item weights and dimensions:
- Weight: sum of
(item weight * quantity), converted to ounces (1 lb = 16 oz); default 1 lb per item - Length/width: maximum across all items; defaults: 10" x 8"
- Height: sum of item heights * quantities, capped at 24"; default 2" per item
- Weight: sum of
- Carrier API call - Build
ShipmentRequestwith origin address and destination, callgetShippingRates() - PO Box filter - If destination is a PO Box, filter to USPS-only rates
- Sort and return - Sort rates by cost (cheapest first), return all as
availableOptions - Fallback - On any error, return estimate-based rates with
fallback: trueflag
Estimate-Based Rates (Fallback)
Used when the carrier API is unavailable or returns no rates.
| Condition | Rate |
|---|---|
| Subtotal >= free shipping threshold | $0.00 (Free Shipping) |
| International (non-US) | $24.99 |
| Standard domestic (flat rate) | $6.99 |
| Weight > 5 lbs | $4.99 base + $0.50/lb over 5 lbs |
| Express shipping | $14.99 |
State Multipliers
Remote states have a cost multiplier applied:
| State | Multiplier |
|---|---|
| Alaska (AK) | 1.5x |
| Hawaii (HI) | 1.5x |
| Puerto Rico (PR) | 2.0x |
PO Box Estimate Options
When the destination is a PO Box, only USPS options are offered:
| Service | Rate |
|---|---|
| USPS Ground Advantage | base rate |
| USPS Priority Mail | base rate x 1.5 |
| USPS Priority Mail Express | express rate |
Non-PO-Box Estimate Options
| Service | Rate |
|---|---|
| Standard Shipping | base rate |
| Express Shipping | express rate |
Fallback Rate System
The system has multiple fallback layers to ensure checkout is never blocked:
- Carrier API succeeds - Use real carrier rates
- Carrier API returns no rates - Use estimate-based rates with
fallback: true - Carrier API errors - Log error, use estimate-based rates with
fallback: true - API key missing -
getShippingClient()throws; caught bygetShippingRates(), returns empty rates; calculator falls back to estimates - Calculate-shipping endpoint fails entirely - Returns HTTP 200 with flat-rate estimate ($6.99) rather than HTTP 500
- Free shipping threshold DB fetch fails - Falls back to hardcoded
$50.00threshold
PO Box Handling
The system detects PO Box addresses using pattern matching on the address line.
Detection Patterns
The following patterns (case-insensitive, periods removed) trigger PO Box detection:
PO BOX,P.O. BOX,P O BOXPOST OFFICE BOXPOB,P.O.BBOX 123(only at start of address)
Behavior When PO Box Detected
- Carrier API mode: Filters rates to USPS-only (UPS and FedEx cannot deliver to PO Boxes)
- Fallback mode: Returns USPS Ground Advantage, Priority Mail, and Priority Mail Express options only
- If no USPS rates are available from the carrier API, falls back to estimate rates
Caching
Server-Side In-Memory Cache
Location: app/api/checkout/calculate-shipping/route.ts
The shipping calculation endpoint caches results in memory to avoid redundant carrier API calls during a checkout session.
| Parameter | Value |
|---|---|
| TTL | 5 minutes |
| Max entries | 100 |
| Cache key | Sorted productId:quantity pairs + city|state|postalCode |
Cache pruning runs after each new entry. Expired entries are removed first; if still over the limit, oldest entries are evicted.
Client-Side Debouncing
The checkout page debounces shipping calculation requests by 800ms as the user types address fields (city, state, ZIP code).
Address Validation
validateShippingAddress() in lib/shipping-calculator.ts
Validates address fields with the following rules:
| Field | Rule |
|---|---|
address1 | Required, minimum 3 characters |
city | Required, minimum 2 characters |
state | Required, exactly 2 characters (e.g., CA, NY) |
postalCode | Required, minimum 5 characters |
country | Required, exactly 2 characters (e.g., US) |
Returns { valid: boolean, errors: string[] }.
API-Level Validation
The calculate-shipping endpoint validates with Zod:
items: non-empty array of{ productId: CUID, quantity: positive int }shippingAddress.state: exactly 2 lettersshippingAddress.postalCode: minimum 5 charactersshippingAddress.country: defaults toUS
Key Files
| File | Purpose |
|---|---|
lib/shipping-calculator.ts | Core shipping calculation logic, rate computation, fallback system |
lib/shipping-api.ts | Carrier API client (EasyPost/Shippo), rate fetching, configuration validation |
lib/shipping-carriers.ts | Carrier constants (usps, ups, fedex) and display labels |
app/api/checkout/calculate-shipping/route.ts | HTTP endpoint for real-time shipping estimates with caching |
app/admin/settings/shipping/page.tsx | Admin UI for configuring shipping settings |
How is this guide?
Last updated on