Architecture Overview
TwelveTake Cart is a self-hosted, white-label e-commerce platform built on:
| Layer | Technology |
|---|---|
| Framework | Astro 5 SSR (@astrojs/node standalone adapter) |
| Database | SQLite via better-sqlite3 (synchronous, file-based) |
| Styling | Tailwind CSS 4 (via Vite plugin) |
| Nodemailer (SMTP) | |
| Image Processing | Sharp (resize, format conversion) |
| PDF Generation | PDFKit (invoices, packing slips, 1099s) |
Request Flow
Browser → Astro SSR → Middleware (session, CSRF, admin auth) → Page/API Handler → SQLite All pages are server-rendered. No client-side framework — interactive elements use vanilla JS with <script is:inline> blocks. The storefront uses Astro's View Transitions for SPA-like navigation.
Key Design Principles
- White-label: No hardcoded branding. Every label, color, and URL is configurable.
- All-in-one: Every feature is built-in. No plugins or paid add-ons.
- Payment-agnostic: Pluggable processor registry. Sites can use Square, Stripe, PayPal, or custom processors.
- Self-hosted: Runs on your own infrastructure. No third-party SaaS dependencies for core functionality.
- Settings as key-value pairs: The
store_settingstable stores all configuration, making it easy to add new settings without schema changes.
Getting Started
Prerequisites
- Node.js 18+ (22 recommended)
- npm or pnpm
Installation
git clone <repo-url> cart
cd cart
npm install Development
npm run dev # Start dev server on http://localhost:4321
npm run build # Production build
npm run preview # Preview production build
npm run test:a11y # Run accessibility tests (Playwright + axe-core) Database Setup
The database is created automatically on first run. To seed with test data:
node scripts/seed-test-data.js This creates sample customers, products, orders, categories, and test data for all feature modules. The database file lives at data/store.db. Delete it and re-seed to start fresh.
Environment Variables
Create a .env file for payment processor credentials and other secrets:
# Square Sandbox (for development)
SQUARE_SANDBOX_ID=sandbox-sq0idb-...
SQUARE_SANDBOX_TOKEN=EAAAl...
SQUARE_SANDBOX_LOCATION=L...
# Stripe (if using)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
# PayPal (if using)
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=... Project Structure
cart/
├── src/
│ ├── pages/
│ │ ├── api/ # API endpoints (REST)
│ │ │ ├── shop/ # Customer-facing APIs (cart, reviews, wishlist)
│ │ │ └── store/ # Admin APIs (products, orders, settings)
│ │ ├── shop/ # Storefront pages (customer-facing)
│ │ └── store/ # Admin panel pages
│ ├── layouts/
│ │ ├── ShopLayout.astro # Storefront layout (header, footer, cart drawer)
│ │ └── AdminLayout.astro # Admin panel layout (sidebar nav)
│ ├── components/ # Reusable UI components
│ ├── lib/ # Business logic & data layer
│ │ └── payments/ # Payment processor implementations
│ ├── styles/
│ │ └── global.css # Global styles, CSS variables
│ └── middleware/ # Astro middleware (session, auth, CSRF)
├── data/ # SQLite database & uploads
│ ├── store.db # Main database
│ └── uploads/ # Media library files
├── public/ # Static assets (favicon, robots.txt)
├── docs/ # Documentation
│ └── api/
│ └── openapi.yaml # OpenAPI 3.0 specification
├── hooks/ # Custom event hook files (optional)
├── scripts/ # Utility scripts (seeding, migrations)
└── tests/ # Test suites
└── a11y/ # Accessibility tests (Playwright) Routing Convention
Astro file-based routing maps directly to URLs:
| File | URL |
|---|---|
src/pages/shop/products/[slug].astro | /shop/products/cool-shirt |
src/pages/api/store/cart/add.ts | POST /api/store/cart/add |
src/pages/store/orders/[id].astro | /store/orders/42 |
Authentication
The platform supports three authentication methods:
1. Session Cookie (Browser)
Used by the admin panel and customer accounts. Sessions are HTTP-only cookies set at login.
Cookie: session=<session_id> Sessions are managed in the sessions table. Configurable settings:
session_max_age_days— Maximum session lifetime (default: 7 days)session_idle_timeout_minutes— Inactivity timeout (default: 120 minutes)
The middleware (src/middleware/index.ts) attaches session data to Astro.locals:
// Available in all pages and API routes:
Astro.locals.sessionId // string -- session ID
Astro.locals.customerId // number | null -- logged-in customer
Astro.locals.adminUser // object | null -- logged-in admin
Astro.locals.csrfToken // string -- CSRF token for forms 2. API Key (External Integrations)
For server-to-server integrations. Create keys in Settings > API Keys.
Authorization: Bearer tt_live_abc123... Keys are scoped — each key can be granted read/write access to specific resource types:
| Scope | Resources |
|---|---|
products:read | Product catalog |
products:write | Product CRUD |
orders:read | Order data |
orders:write | Order management |
customers:read | Customer profiles |
customers:write | Customer management |
inventory:read | Stock levels |
inventory:write | Stock updates |
settings:read | Store configuration |
settings:write | Store configuration updates |
Write scope implies read. Keys can be revoked at any time from the admin panel.
3. Token (One-Time Links)
Some endpoints accept a single-use token in the URL (e.g., download links, email confirmation, password reset, quote approval). These are system-generated and not user-managed.
CSRF Protection
All POST/PUT/PATCH/DELETE requests from the browser must include a CSRF token:
- Forms: Include
<input type="hidden" name="_csrf" value={Astro.locals.csrfToken} /> - AJAX: The token is available in
document.querySelector('meta[name="csrf-token"]')?.content
API key-authenticated requests are exempt from CSRF checks.
API Reference
The full API specification is available as an OpenAPI 3.0 document included with every install. Interactive docs are served at /store/api-docs in the admin panel via ReDoc.
Response Format
All JSON endpoints return a standard envelope:
Success:
{
"success": true,
"data": { ... }
} Paginated:
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"per_page": 20,
"total": 57,
"total_pages": 3
}
} Error:
{
"success": false,
"error": "Human-readable description",
"error_code": "MACHINE_READABLE_CODE"
} Common Error Codes
| Code | HTTP Status | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Not authenticated |
FORBIDDEN | 403 | Insufficient permissions |
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 400 | Invalid input |
VALIDATION_MISSING_FIELD | 400 | Required field missing |
RATE_LIMITED | 429 | Too many requests |
INTERNAL_ERROR | 500 | Server error |
Rate Limiting
| Endpoint | Limit |
|---|---|
| Login | 10 attempts / 5 minutes per IP |
| Registration | 5 attempts / 10 minutes per IP |
| Other endpoints | Not rate-limited (configurable) |
Key API Endpoints
Cart:
GET /api/store/cart— Get current cartPOST /api/store/cart/add— Add item to cartPOST /api/store/cart/update— Update item quantityPOST /api/store/cart/remove— Remove item from cart
Checkout:
GET /api/store/checkout/shipping-methods— Get available shipping methodsPOST /api/store/checkout— Place order
Products:
GET /api/shop/products— List products (paginated, filterable)GET /api/shop/products/:slug— Get product detail
Orders:
GET /api/shop/orders— Customer order historyGET /api/store/orders/:id— Admin order detail
Import:
POST /api/store/import/woocommerce— WooCommerce CSV importPOST /api/store/import/shopify— Shopify CSV importPOST /api/store/import/csv-mapper— Generic CSV import with column mapping
The full specification covers 230+ endpoints.
Webhooks & Events
Webhooks (External Notifications)
Configure webhooks in Settings > Webhooks to receive HTTP POST notifications when events occur.
Supported Events:
order.created,order.updated,order.shipped,order.completed,order.cancelledproduct.created,product.updated,product.deletedcustomer.created,customer.updatedreturn.requested,return.approved,return.receivedinventory.low_stocksubscription.created,subscription.renewed,subscription.cancelled
Payload Format:
{
"event": "order.created",
"timestamp": "2026-03-12T14:30:00Z",
"data": { ... }
} Security: Each webhook includes an HMAC-SHA256 signature in the X-Webhook-Signature header. Verify this signature on your server to authenticate the request.
Retry Logic: Failed deliveries (non-2xx response) are retried with exponential backoff. Delivery logs are viewable in the admin panel.
Event Hooks (Internal Extensibility)
The internal hook system allows custom business logic to run before or after key operations.
- Before hooks — Awaited. Can modify or cancel the operation by throwing.
- After hooks — Fire-and-forget. For notifications, logging, side effects.
File-Based Loading: Place .ts or .js files in the hooks/ directory. They're loaded automatically at startup.
// hooks/order-notifications.ts
import { registerHook } from '../src/lib/hooks';
registerHook('after', 'order.shipped', async (data) => {
// Send SMS notification, update external system, etc.
console.log(`Order ${data.order.order_number} shipped!`);
}); Available Hook Points:
order.create,order.update,order.shipped,order.completedproduct.created,product.updatedcustomer.createdreturn.requested,return.approvedinventory.low_stock
Payment Processor Integration
Payment processors are pluggable via a registry pattern. Each processor implements the PaymentProcessor interface.
Built-In Processors
| Processor | Features |
|---|---|
| Square | Payments, refunds, subscriptions |
| Stripe | Payments, refunds, subscriptions |
| PayPal | Payments, refunds |
| Offline | Manual/check/PO payments |
Adding a Custom Processor
Create a new file in src/lib/payments/:
// src/lib/payments/my-processor.ts
import { registerProcessor, type PaymentProcessor } from '../payment';
const myProcessor: PaymentProcessor = {
id: 'my_processor',
name: 'My Payment Service',
// Fields shown in Settings > Payment
fields: [
{ key: 'my_processor_api_key', label: 'API Key', type: 'password', required: true },
{ key: 'my_processor_merchant_id', label: 'Merchant ID', type: 'text', required: true },
],
async processPayment(order, amount, config) {
const apiKey = config.my_processor_api_key;
// Call your payment API...
return { success: true, transactionId: 'txn_123', status: 'completed' };
},
async processRefund(transactionId, amount, config) {
return { success: true, refundId: 'ref_123' };
},
getClientConfig(config) {
return { merchantId: config.my_processor_merchant_id };
},
};
registerProcessor(myProcessor); Import the file in src/lib/payment.ts so it registers at startup. The processor will automatically appear in Settings > Payment with its configured fields.
Subscription Adapter
For recurring billing, processors can optionally implement SubscriptionPaymentAdapter:
interface SubscriptionPaymentAdapter {
createSubscription(plan, customer, paymentMethod, config): Promise<SubscriptionResult>;
cancelSubscription(subscriptionId, config): Promise<void>;
chargeRenewal(subscription, amount, config): Promise<PaymentResult>;
} Database
Engine
SQLite via better-sqlite3. Synchronous API. The database file lives at data/store.db.
Schema Management
Schema is defined in src/lib/db.ts via CREATE TABLE IF NOT EXISTS statements. Migrations are applied automatically on startup — new tables and columns are added; existing data is preserved.
Key Tables
| Table | Purpose |
|---|---|
products | Product catalog |
product_variants | Size/color/style variants |
product_options, product_option_values | Option definitions (Color, Size) |
orders | Order header (totals, status, customer) |
order_items | Line items per order |
order_events | Status changes, notes, audit trail |
customers | Customer accounts |
store_settings | Key-value configuration |
promotions | Coupons, auto-discounts, BOGO rules |
subscriptions | Recurring billing subscriptions |
webhooks | Registered webhook URLs |
api_keys | API key credentials and scopes |
admin_users | Admin/staff accounts |
roles, permissions | RBAC for admin users |
audit_log | Admin action audit trail |
loyalty_ledger | Points transactions (append-only) |
vendor_products | Vendor-to-product assignments |
merch_product_types | Merch product type definitions |
Accessing the Database
import { getDb } from '@lib/db';
const db = getDb();
const products = db.prepare('SELECT * FROM products WHERE status = ?').all('active'); All database operations are synchronous (better-sqlite3). For complex operations, use transactions:
const tx = db.transaction(() => {
db.prepare('UPDATE ...').run(...);
db.prepare('INSERT ...').run(...);
});
tx(); Configuration & Settings
All configuration is stored in the store_settings table as key-value pairs.
Reading Settings
import { getSetting, getAllSettings } from '@lib/store';
const storeName = getSetting('store_name');
const allSettings = getAllSettings(); // Returns Record<string, string> Writing Settings
import { setSettings } from '@lib/store';
setSettings({
store_name: 'My Store',
currency_code: 'USD',
currency_symbol: '$',
}); Key Settings Reference
| Key | Default | Description |
|---|---|---|
store_name | — | Store display name |
currency_code | USD | 3-letter ISO 4217 currency |
currency_symbol | $ | Currency display symbol |
brand_color | #111827 | Primary brand color (hex) |
payment_processor | — | Active processor ID |
low_stock_threshold | 5 | Global low-stock alert level |
promotion_stacking_mode | best_wins | none, best_wins, or controlled |
cart_recovery_enabled | 0 | Enable abandoned cart emails |
b2b_enabled | 0 | Enable B2B features |
merch_builder_enabled | 0 | Enable merch builder module |
Features are toggled on/off via settings. Disabling a feature hides its UI and ignores its logic — no code changes needed.
Media System
The media library handles file uploads, image processing, and serving.
Image Processing
- Max dimensions: 1920px (longest edge)
- Format: JPEG at 80% quality (configurable)
- Variants:
original,thumbnail(200px),medium(800px) - Supported formats: JPEG, PNG, WebP, SVG, GIF
Media Library Admin
The admin media library at /store/media/ provides drag-and-drop upload, folder organization, bulk operations, image preview with alt text editing, and a file picker component for embedding in forms.
Email System
Email is sent via Nodemailer using SMTP. Configure in the setup wizard or directly in settings.
Transactional Emails
The system sends emails for order confirmation, shipment notification, password reset, email verification, return request updates, subscription renewal and payment failure, cart abandonment recovery, gift card delivery, and loyalty point notifications.
Email Templates
Templates are customizable in Settings > Notifications using placeholder variables like {store_name}, {order_number}, {customer_name}, {tracking_url}, and {total}.
Extending the Platform
Adding a New Admin Page
Create an .astro file in src/pages/store/ and use the AdminLayout.
Adding a New API Endpoint
Create a .ts file in src/pages/api/:
import type { APIRoute } from 'astro';
import { apiSuccess, apiUnauthorized } from '@lib/api-response';
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.adminUser) return apiUnauthorized();
const body = await request.json();
// Your logic...
return apiSuccess({ result: 'done' });
}; Adding a New Setting
Add the key to the POST handler's allowed list, add the UI field, and read it with getSetting('my_new_setting'). No database migration needed.
Adding Custom Fields
Custom fields can be defined for products, orders, and customers via Settings > Custom Fields. These are stored in a generic table and rendered automatically in the admin UI.
Deployment
Build & Run
npm run build # Produces standalone Node.js server
node dist/server/entry.mjs # Listens on port 4321 (configurable via PORT) Production Checklist
- Set
NODE_ENV=production - Configure real SMTP credentials (not sandbox)
- Switch payment processor to live mode
- Set a strong admin password
- Configure
store_name,currency_code,default_country - Upload logo and favicon in Settings > Appearance
- Set up webhooks for any external integrations
- Enable HTTPS via reverse proxy (nginx, Caddy)
- Set up database backups (the
data/directory) - Run the setup wizard at
/store/setup/
Reverse Proxy (nginx)
server {
listen 443 ssl;
server_name shop.example.com;
ssl_certificate /etc/ssl/certs/shop.pem;
ssl_certificate_key /etc/ssl/private/shop.key;
location / {
proxy_pass http://127.0.0.1:4321;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
} Backups
Back up the data/ directory regularly. It contains store.db (the entire database) and uploads/ (all uploaded media files). SQLite supports hot backups — you can copy the file while the server is running.