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
- No free shipping - Shipping is charged on every order, without exception
- Configurable - The warehouse origin address and enabled carriers 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
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:
- 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
- Origin resolution - Read the warehouse address from
ShippingSettings, thenSHIPPING_ORIGIN_*; an incomplete address falls through to estimates rather than quoting from a placeholder - 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 the cheapest as the single entry in
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 |
|---|---|
| International (non-US) | $24.99 |
| Standard domestic (flat rate) | $6.99 |
| Weight > 5 lbs | $4.99 base + $0.50/lb over 5 lbs |
All rates above are carriage only. A flat $4.00 packaging-and-materials fee
(lib/shipping/handling-fee.ts) is added once per order to every quote the calculator returns —
live carrier rates included — so the domestic flat rate reaches the customer as $10.99 and the
international rate as $28.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 Option
When the destination is a PO Box, the single option is USPS-only:
| Service | Rate |
|---|---|
| USPS Ground Advantage | base rate |
Non-PO-Box Estimate Option
| Service | Rate |
|---|---|
| Standard Shipping | base 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 the flat-rate estimate plus the packaging fee ($10.99) rather than HTTP 500
- Origin address missing or incomplete - Falls back to flat-rate estimates and logs which fields are absent
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 the USPS Ground Advantage option 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: required, whitespace trimmed; minimum 5 characters whencountryisUS. Shorter formats are accepted for other countries — Australian postcodes are four digits, and rejecting them blocked international quotes the calculator has a published rate for.shippingAddress.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