API Documentation
The Storefront API is a read-only service that powers product search, filtering, and category browsing for the storefront. It reads from a shared Neon PostgreSQL database and caches frequently accessed data in Upstash Redis.
https://storefront-api-eight.vercel.appLocal:
http://localhost:3000Architecture
Browser / Storefront
│
▼
Vercel CDN (Edge) ← HTTP Cache-Control headers
│
▼
Next.js Serverless ← API routes (us-east-1)
│
┌────┴────┐
▼ ▼
Neon DB Upstash Redis
(shared) (shared)
│ │
└────┬────┘
│
shift4shop-sync ← Writes to DB, invalidates cache- Next.js 14 — API routes deployed as Vercel serverless functions
- Neon PostgreSQL — ~50k products, connection pool (max 10), auto-suspend enabled
- Upstash Redis — caches expensive queries (filters, products, settings)
- Vercel CDN — edge caching via Cache-Control headers on API responses
DB Fallback
When Neon PostgreSQL is unavailable, the API automatically falls back to the Shift4Shop REST API so the live site continues serving product data.
Normal: Request → Redis → Neon DB ✓ Redis down: Request → Neon DB ✓ (cache miss) Neon down: Request → Redis miss → Shift4Shop API ✓
Routes with fallback
/api/productsand/api/filters— full fidelity (in-memory filtering)/api/categories/[id]/settings— full fidelity
How to detect fallback
_sourcefield in response body —"db"or"api"X-Data-Sourceresponse header- Vercel Runtime Logs:
[fallback] Serving /api/... from Shift4Shop API
Limitations in fallback mode
- Higher latency (~500ms–1s vs ~50ms from Neon)
- Rate limited (~60 req/min on Shift4Shop API)
Security
Authentication
Public catalog routes require no authentication. Dashboard and admin routes require a session cookie obtained via /api/auth/login.
Protected routes
POST /api/cache— cache invalidationPOST /api/fallback/override— fallback mode control/api/auth/users— user management/dashboard/*— all dashboard pages (redirects to /login)
CORS
All /api/* routes are protected by CORS origin allowlisting, enforced in middleware.
Allowed origins
- Origins listed in
ALLOWED_ORIGINSenv var - Vercel deployment URLs (auto-detected)
localhost:3000andlocalhost:3001in development
Same-origin requests
Requests without an Origin header are always allowed (server-to-server, curl, etc.)
Rate Limiting
Public API routes are rate limited at 60 requests per minute per IP address using a Redis fixed-window counter.
- Returns
429 Too Many RequestswithRetry-Afterheader when exceeded - Fail-open design — if Redis is unavailable, requests pass through
- Auth routes (
/api/auth/*) are excluded from rate limiting
Error Responses
All error responses return generic messages — internal error details are never exposed to clients.
Caching Strategy
Three-tier caching reduces database load and improves response times:
| Layer | Where | Details |
|---|---|---|
| Vercel CDN | Edge (global) | Honors Cache-Control headers (30-60s on search routes) |
| Upstash Redis | Application | 5-10min TTL on filters, products, category settings |
| Neon PostgreSQL | Origin | Connection pool (max 10), query-level caching by Neon |
POST /api/cache after product syncs. Manual invalidation is available from the dashboard.Data Model
Key field mappings from the Shift4Shop data model:
| DB Field | Display Label | Notes |
|---|---|---|
| extra_field1 | (Sort Order) | Numeric sort weight — used for default product ordering |
| extra_field2 | Lead Time | e.g., “Ships in 3-5 days” |
| extra_field3 | Assembly Type | e.g., “Ready To Assemble”, “Pre-Assembled” |
| extra_field4 | Made in the USA | “Yes” / “No” |
| extra_field5 | Door Style | e.g., “Shaker”, “Raised Panel” |
| manufacturer_name | Series Name | e.g., “Elegance Series” |
| distributor_list | Color | JSONB array — extracted via DistributorName field |
| category_special | Best Seller | Boolean — enables best_seller sort option |
| has_related | (computed) | Boolean — true if product has related products in product_related table |
| related_catalogid | (computed) | ID of first related product — used for preview prefetch on hover |
| non_searchable | (visibility) | Visible in category browse, hidden from full-site search |
API Reference
Detailed documentation for each endpoint.
/api/search30s HTTP cache via Cache-Control headerProduct Search
Full-text product search with keyword matching, multi-select filters, and pagination. Serves both full-site search and category-scoped browsing.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q | string | Search keyword — matches product name, description, and SKU via full-text search |
| category_id | integer | Scope results to a category. When set, non_searchable products are included |
| page | integer | Page number (default: 1) |
| limit | integer | Results per page, max 100 (default: 12) |
| sort | string | Sort mode: default, relevance, name, price_asc, price_desc, newest, best_seller, stock |
| ids | string | Comma-separated product IDs for custom sort order (Shift4Shop template integration) |
| min_price | number | Minimum price filter |
| max_price | number | Maximum price filter |
| in_stock | boolean | Filter to in-stock products only (true) |
| manufacturer_name | string | Filter by Series Name (multi-select: repeat param) |
| color | string | Filter by Color — extracted from JSONB distributor_list (multi-select) |
| extra_field2 | string | Filter by Lead Time (multi-select) |
| extra_field3 | string | Filter by Assembly Type (multi-select) |
| extra_field4 | string | Filter by Made in the USA (multi-select) |
| extra_field5 | string | Filter by Door Style (multi-select) |
| feature_* | string | Dynamic feature filter — e.g., feature_Color=Red, feature_Cabinet_Type=Base |
Example
/api/search?q=modern+white&category_id=1796&sort=relevance&limit=12Response
{
"success": true,
"products": [
{
"id": 62143,
"name": "Modern White 10x10 Set",
"description": "...",
"price": 1899.99,
"sale_price": 1499.99,
"sku": "MW-10X10",
"stock": 25,
"on_sale": true,
"main_image": "/assets/images/mw-10x10.jpg",
"thumbnail": "/assets/images/mw-10x10-thumb.jpg",
"manufacturer_name": "Elegance Series",
"color": "White",
"extra_field1": "10",
"extra_field2": "Ships in 3-5 days",
"extra_field3": "Ready To Assemble",
"extra_field4": "Yes",
"extra_field5": "Shaker",
"category_special": false,
"has_related": true,
"related_catalogid": 62144
}
],
"pagination": {
"page": 1,
"limit": 12,
"total": 47,
"total_pages": 4,
"has_more": true
}
}Notes
- Multi-select filters: repeat the param (e.g., extra_field2=Ships+in+3+days&extra_field2=Ships+in+5+days)
- Hidden products (hide=true) are always excluded
- Non-searchable products are excluded from full-site search but included when category_id is set
- Relevance sort uses PostgreSQL ts_rank() on the search query
- Default sort uses extra_field1 as numeric sort order (admin-configured)
/api/search/filtersRedis — 5 minute TTL (key built from sorted params)Filter Options
Returns available filter values with product counts, contextual to currently active filters. Each filter group's counts reflect all other active filters (cross-filter counting).
Parameters
| Parameter | Type | Description |
|---|---|---|
| q | string | Search keyword (same as /api/search) |
| category_id | integer | Scope to a category |
| manufacturer_name | string | Active Series Name filter(s) |
| color | string | Active Color filter(s) |
| extra_field2 | string | Active Lead Time filter(s) |
| extra_field3 | string | Active Assembly Type filter(s) |
| extra_field4 | string | Active Made in USA filter(s) |
| extra_field5 | string | Active Door Style filter(s) |
| min_price | number | Minimum price filter |
| max_price | number | Maximum price filter |
Example
/api/search/filters?category_id=1796&extra_field3=Ready+To+AssembleResponse
{
"success": true,
"price_range": { "min": 49.99, "max": 3299.99 },
"categories": [
{ "id": 1796, "name": "RTA Kitchen Cabinets", "count": 70 }
],
"filter_groups": [
{
"key": "manufacturer_name",
"label": "Series Name",
"type": "checkbox",
"options": [
{ "value": "Elegance Series", "label": "Elegance Series", "count": 12 },
{ "value": "Heritage Series", "label": "Heritage Series", "count": 8 }
]
},
{
"key": "extra_field5",
"label": "Door Style",
"type": "checkbox",
"options": [
{ "value": "Shaker", "label": "Shaker", "count": 35 },
{ "value": "Raised Panel", "label": "Raised Panel", "count": 15 }
]
}
]
}Notes
- Runs 6+ parallel queries (price range, each filter field, categories, features)
- Zero-count options are excluded from results
- Cross-filter counting: selecting "Shaker" updates Color counts but Door Style still shows all options
/api/search/suggest30s HTTP cacheAutocomplete Suggestions
Returns categorized autocomplete suggestions for products, categories, and SKUs. Designed for search-as-you-type UI.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q* | string | Search term (minimum 2 characters) |
| category_id | integer | Optional scope to a category |
Example
/api/search/suggest?q=modernResponse
{
"success": true,
"suggestions": [
{ "text": "Modern White Base 24\"", "type": "product", "id": 62143, "meta": "$189.99" },
{ "text": "Kitchen Products", "type": "category", "id": 1796, "meta": "70 products" },
{ "text": "MW-B24", "type": "sku", "id": 62143, "meta": "Modern White Base" }
]
}Notes
- Returns up to 3-5 results per type (product, category, SKU)
- Uses both full-text search and ILIKE pattern matching
- Excludes hidden and non_searchable products
/api/preview60s HTTP cacheProduct Quick-View
Fetches full product details plus its first related product for modal previews. Includes images and feature attributes. Shared module — used by both search and category browsing.
Parameters
| Parameter | Type | Description |
|---|---|---|
| id* | integer | Product catalog ID |
Example
/api/preview?id=62143Response
{
"success": true,
"product": {
"id": 62143,
"name": "Modern White 10x10 Set",
"short_description": "...",
"description": "...",
"price": 1899.99,
"sale_price": 1499.99,
"on_sale": true,
"main_image": "/assets/images/product-main.jpg",
"thumbnail": "/assets/images/product-thumb.jpg",
"manufacturer_name": "Elegance Series",
"extra_field1": "10", "extra_field2": "Ships in 3-5 days",
"extra_field3": "Ready To Assemble", "extra_field4": "Yes",
"extra_field5": "Shaker",
"sample_enable": 1, "sample_name": "Sample", "sample_price": 4.99,
"images": [
{ "image_file": "/assets/images/product-1.jpg", "image_caption": "Front view" }
],
"features": [
{ "feature_name": "Collection", "feature_value": "Elegance" },
{ "feature_name": "Color Family", "feature_value": "White" }
]
},
"relatedProduct": {
"id": 62144,
"name": "Sample Product",
"...": "same structure as product"
}
}Notes
- Related products linked via product_related table (relationship_type = related)
- relatedProduct is the first related product found (if any)
- Fetches product_images and product_features for both the main product and related product
- Use has_related + related_catalogid from listing APIs to prefetch on hover
/api/productsRedis — 5 minute TTL (key: products:category:{id}:{params})Products (Data Layer)
Raw paginated products for a category. Returns product data only — no filter options. Use /api/filters for products + filter options together. Falls back to Shift4Shop REST API when DB is down.
Parameters
| Parameter | Type | Description |
|---|---|---|
| category* | integer | Category ID |
| page | integer | Page number (default: 1) |
| limit | integer | Results per page, max 100 (default: 6) |
| sort | string | Sort: featured (default), price_asc, price_desc, name_asc, name_desc, newest, best_seller |
| ids | string | Comma-separated product IDs for native Shift4Shop sort order |
| [filter_name] | string | Any other param is treated as a filter. Multi-value supported: repeat param for OR logic (e.g., Color=White&Color=Gray). |
Example
/api/products?category=1796&limit=12&sort=featured&Color=WhiteResponse
{
"products": [
{
"catalogid": 62143,
"sku": "MW-10X10",
"name": "Modern White 10x10 Set",
"price": 1899.99,
"sale_price": 1499.99,
"on_sale": true,
"main_image": "/assets/images/product-main.jpg",
"thumbnail": "/assets/images/product-thumb.jpg",
"manufacturer_name": "Elegance Series",
"color": "White",
"category_special": false,
"stock": 25,
"extra_field1": "10", "extra_field2": "Ships in 3-5 days",
"has_related": true,
"related_catalogid": 62144,
"features": {}
}
],
"total": 33,
"page": 1,
"totalPages": 3,
"_source": "db"
}Notes
- Data layer endpoint — returns products only, no filter options
- Falls back to Shift4Shop REST API when Neon DB is down — check _source field
- The ids param supports Shift4Shop's native sort order via array_position()
- best_seller sort uses category_special boolean column
/api/filtersRedis — 5 minute TTL (key built from sorted params, excludes ids)Category Filters (Composite)
Composite endpoint: returns paginated products AND dynamic filter options for a category in a single request. Auto-detects filter mode: uses product_features if available, otherwise filters on extra_field columns and distributor_list. Ensures data consistency between products and filter counts.
Parameters
| Parameter | Type | Description |
|---|---|---|
| category* | integer | Category ID |
| page | integer | Page number (default: 1) |
| limit | integer | Results per page, max 100 (default: 6) |
| sort | string | Sort: featured (default), price_asc, price_desc, name_asc, name_desc, newest, best_seller |
| ids | string | Comma-separated product IDs for native Shift4Shop sort order |
| [filter_name] | string | Any other param is treated as a filter. Multi-value supported: repeat param for OR logic (e.g., Color=White&Color=Gray). Filter names match labels: Lead Time, Assembly Type, Series, Color, etc. |
Example
/api/filters?category=1796&limit=12&sort=featured&Color=White&Color=GrayResponse
{
"products": [
{
"catalogid": 62143,
"sku": "MW-10X10",
"name": "Modern White 10x10 Set",
"price": 1899.99,
"sale_price": 1499.99,
"on_sale": true,
"main_image": "/assets/images/product-main.jpg",
"thumbnail": "/assets/images/product-thumb.jpg",
"manufacturer_name": "Elegance Series",
"color": "White",
"category_special": false,
"stock": 25,
"extra_field1": "10", "extra_field2": "Ships in 3-5 days",
"has_related": true,
"related_catalogid": 62144,
"features": {}
}
],
"filters": [
{
"name": "Color",
"options": [
{ "value": "White", "count": 13 },
{ "value": "Gray", "count": 20 }
]
}
],
"total": 33,
"page": 1,
"totalPages": 3,
"_source": "db"
}Notes
- Composite endpoint — products + filter options in one call for data consistency
- Auto-detects filter mode: product_features (e.g., flooring) or extra_field columns (e.g., RTA cabinets)
- Multi-value filters: repeat the param for OR logic (e.g., Color=White&Color=Gray)
- The ids param supports Shift4Shop's native sort order via array_position()
- Falls back to Shift4Shop REST API when Neon DB is down — check _source field
- best_seller sort uses category_special boolean column
/api/categories/[id]No caching (direct DB query)Category Products (Legacy)
Legacy API for category product listings with filtering. Still used by the live Shift4Shop storefront.
Parameters
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Results per page (default: 20) |
| sortBy | string | Sort field: catalogid, name, price, sku |
| sortOrder | string | Sort direction: asc, desc |
| extra_field1-5 | string | Multi-select filter arrays |
| min_price | number | Minimum price filter |
| max_price | number | Maximum price filter |
| feature_* | string | Product feature filters (e.g., feature_Number_of_Doors=2) |
Example
/api/categories/27263?page=1&limit=20&sortBy=price&sortOrder=ascResponse
{
"success": true,
"category": {
"category_id": 27263,
"name": "Flooring",
"description": "...",
"product_count": 12
},
"products": [
{
"id": 62200, "name": "...", "price": 3.49, "sale_price": 2.99, "on_sale": true,
"main_image": "...", "thumbnail": "...", "sku": "...", "stock": 100,
"manufacturer_name": "...", "color": "Nordic Blonde",
"extra_field1": "28 mil", "has_related": false, "related_catalogid": null
}
],
"pagination": {
"page": 1, "limit": 20, "total": 12, "totalPages": 1,
"hasNextPage": false, "hasPrevPage": false
},
"filters": { "sortBy": "price", "sortOrder": "asc" }
}Notes
- Legacy endpoint — newer integrations should use /api/search with category_id
- sortBy values are sanitized to prevent SQL injection
/api/categories/[id]/settingsRedis — 10 minute TTLCategory Settings
Returns category-specific display configuration including default sort order, products per page, grid columns, and display title.
Example
/api/categories/27263/settingsResponse
{
"category_id": 27263,
"category_name": "Flooring",
"default_products_sorting": 0,
"items_per_page": 9,
"product_columns": 3,
"title": "Luxury Vinyl Plank Flooring"
}/api/categories/tree5 minute HTTP cache (Cache-Control: s-maxage=300, stale-while-revalidate=600)Category Tree
Returns child categories under a parent with product and subcategory counts. Used by the category explorer in the dashboard.
Parameters
| Parameter | Type | Description |
|---|---|---|
| parent_id | integer | Parent category ID (default: 0 for root categories) |
Example
/api/categories/tree?parent_id=0Response
{
"success": true,
"parent_id": 0,
"categories": [
{
"category_id": 20475,
"category_name": "Shop All Products",
"parent_id": 0,
"hide": false,
"filter_cat": false,
"sorting": 10,
"is_main": false,
"product_count": 2,
"child_count": 0
}
]
}/api/categories5 minute HTTP cache (Cache-Control: s-maxage=300, stale-while-revalidate=600)Category List
Flat list of all non-hidden categories with product counts.
Example
/api/categoriesResponse
{
"success": true,
"data": [
{ "id": 1796, "name": "Kitchen Cabinets", "parent_id": 327, "hide": false, "product_count": 174 }
]
}/api/cacheN/ACache Invalidation
Clears all storefront Redis cache keys. Called automatically by the sync project after product writes, or manually from the dashboard.
Example
POST /api/cacheResponse
{
"success": true,
"deleted": 42
}Notes
- Invalidates keys matching prefixes: storefront:*, products:*, filters:*, category:settings:*
- Requires dashboard session cookie (returns 401 if not authenticated)
/api/healthNo caching (real-time probe)Health Check
Probes database and Redis connectivity with latency measurements and data counts.
Example
/api/healthResponse
{
"status": "healthy",
"db": { "ok": true, "latencyMs": 12 },
"redis": { "ok": true, "latencyMs": 8 },
"counts": { "products": 50000, "categories": 150 },
"timestamp": "2026-03-22T..."
}Notes
- Returns "degraded" if either DB or Redis is unreachable
- Probes both services independently — one can fail without blocking the other