TWELVETAKE CART

Developer Guide

Technical documentation for integrating with, deploying, or extending TwelveTake Cart.

Architecture Overview

TwelveTake Cart is a self-hosted, white-label e-commerce platform built on:

LayerTechnology
FrameworkAstro 5 SSR (@astrojs/node standalone adapter)
DatabaseSQLite via better-sqlite3 (synchronous, file-based)
StylingTailwind CSS 4 (via Vite plugin)
EmailNodemailer (SMTP)
Image ProcessingSharp (resize, format conversion)
PDF GenerationPDFKit (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_settings table 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:

FileURL
src/pages/shop/products/[slug].astro/shop/products/cool-shirt
src/pages/api/store/cart/add.tsPOST /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:

ScopeResources
products:readProduct catalog
products:writeProduct CRUD
orders:readOrder data
orders:writeOrder management
customers:readCustomer profiles
customers:writeCustomer management
inventory:readStock levels
inventory:writeStock updates
settings:readStore configuration
settings:writeStore 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

CodeHTTP StatusMeaning
UNAUTHORIZED401Not authenticated
FORBIDDEN403Insufficient permissions
NOT_FOUND404Resource not found
VALIDATION_ERROR400Invalid input
VALIDATION_MISSING_FIELD400Required field missing
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Server error

Rate Limiting

EndpointLimit
Login10 attempts / 5 minutes per IP
Registration5 attempts / 10 minutes per IP
Other endpointsNot rate-limited (configurable)

Key API Endpoints

Cart:

  • GET /api/store/cart — Get current cart
  • POST /api/store/cart/add — Add item to cart
  • POST /api/store/cart/update — Update item quantity
  • POST /api/store/cart/remove — Remove item from cart

Checkout:

  • GET /api/store/checkout/shipping-methods — Get available shipping methods
  • POST /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 history
  • GET /api/store/orders/:id — Admin order detail

Import:

  • POST /api/store/import/woocommerce — WooCommerce CSV import
  • POST /api/store/import/shopify — Shopify CSV import
  • POST /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.cancelled
  • product.created, product.updated, product.deleted
  • customer.created, customer.updated
  • return.requested, return.approved, return.received
  • inventory.low_stock
  • subscription.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.completed
  • product.created, product.updated
  • customer.created
  • return.requested, return.approved
  • inventory.low_stock

Payment Processor Integration

Payment processors are pluggable via a registry pattern. Each processor implements the PaymentProcessor interface.

Built-In Processors

ProcessorFeatures
SquarePayments, refunds, subscriptions
StripePayments, refunds, subscriptions
PayPalPayments, refunds
OfflineManual/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

TablePurpose
productsProduct catalog
product_variantsSize/color/style variants
product_options, product_option_valuesOption definitions (Color, Size)
ordersOrder header (totals, status, customer)
order_itemsLine items per order
order_eventsStatus changes, notes, audit trail
customersCustomer accounts
store_settingsKey-value configuration
promotionsCoupons, auto-discounts, BOGO rules
subscriptionsRecurring billing subscriptions
webhooksRegistered webhook URLs
api_keysAPI key credentials and scopes
admin_usersAdmin/staff accounts
roles, permissionsRBAC for admin users
audit_logAdmin action audit trail
loyalty_ledgerPoints transactions (append-only)
vendor_productsVendor-to-product assignments
merch_product_typesMerch 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

KeyDefaultDescription
store_nameStore display name
currency_codeUSD3-letter ISO 4217 currency
currency_symbol$Currency display symbol
brand_color#111827Primary brand color (hex)
payment_processorActive processor ID
low_stock_threshold5Global low-stock alert level
promotion_stacking_modebest_winsnone, best_wins, or controlled
cart_recovery_enabled0Enable abandoned cart emails
b2b_enabled0Enable B2B features
merch_builder_enabled0Enable 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.