Overview
Openfront is an enterprise e-commerce platform built on a modern, scalable architecture combining Next.js 15 for the frontend and KeystoneJS 6 for the backend. This hybrid approach provides the best of both worlds: a powerful headless CMS with GraphQL API and a flexible, performant frontend framework.
High-Level Architecture
Technology Stack
Frontend
Next.js 15 - React framework with App Router
TypeScript - Type-safe development
Tailwind CSS - Utility-first styling
shadcn/ui - Component library
Backend
KeystoneJS 6 - Headless CMS framework
GraphQL - API query language
PostgreSQL - Primary database
Prisma - Database ORM (via Keystone)
Infrastructure
S3-Compatible Storage - Image and file storage
Digital Ocean Spaces - Default object storage
Docker - Containerization support
Integrations
Stripe/PayPal - Payment processing
Shippo/ShipEngine - Shipping labels
OAuth 2.0 - Third-party app auth
Webhooks - Event notifications
Directory Structure
Openfront follows a feature-based architecture with clear separation of concerns:
Root Structure
openfront/
├── app/ # Next.js 15 App Router
│ ├── (storefront)/ # Customer-facing routes
│ ├── dashboard/ # Admin interface
│ └── api/ # API routes
├── features/ # Feature modules
│ ├── keystone/ # Backend models & API
│ ├── platform/ # Admin UI components
│ ├── storefront/ # Storefront components
│ └── webhooks/ # Webhook handlers
├── components/ # Shared UI components
├── lib/ # Utility functions
└── public/ # Static assets
Next.js App Router Structure
Openfront uses Next.js 15’s App Router with route groups and parallel routes for advanced layouts:
app/
├── (storefront)/
│ └── [countryCode]/ # Multi-region support
│ ├── (main)/ # Main storefront layout
│ │ ├── page.tsx # Homepage
│ │ ├── products/ # Product pages
│ │ ├── cart/ # Shopping cart
│ │ ├── account/ # Customer account
│ │ └── collections/ # Product collections
│ └── (checkout)/ # Checkout flow
│ └── checkout/
│ └── page.tsx
├── dashboard/
│ ├── (admin)/ # Admin dashboard
│ │ ├── orders/ # Order management
│ │ ├── products/ # Product management
│ │ └── customers/ # Customer management
│ ├── signin/ # Authentication
│ └── layout.tsx
└── api/
└── completion/ # AI completions
The (storefront) and (admin) syntax are route groups in Next.js 15, which organize routes without affecting the URL structure.
Features Directory Organization
The features/ directory contains modular business logic organized by domain:
features/
├── keystone/
│ ├── index.ts # Keystone configuration
│ ├── models/ # Data models (88 files)
│ │ ├── Product.ts
│ │ ├── Order.ts
│ │ ├── User.ts
│ │ └── ...
│ ├── mutations/ # Custom GraphQL mutations
│ ├── access.ts # Permission rules
│ └── lib/ # Backend utilities
├── platform/ # Admin UI features
│ ├── orders/
│ ├── products/
│ └── analytics/
├── storefront/ # Customer-facing features
│ ├── cart/
│ ├── checkout/
│ └── account/
└── webhooks/ # Webhook processing
├── webhook-plugin.ts
└── handlers/
KeystoneJS Backend Architecture
Configuration Entry Point
The main Keystone configuration is defined in features/keystone/index.ts:
features/keystone/index.ts
import { config } from "@keystone-6/core" ;
import { models } from "./models" ;
import { extendGraphqlSchema } from "./mutations" ;
import { statelessSessions } from "./sessions" ;
import { withWebhooks } from "../webhooks/webhook-plugin" ;
export default withAuth (
withWebhooks (
config ({
db: {
provider: "postgresql" ,
url: process . env . DATABASE_URL ,
} ,
lists: models ,
storage: {
my_images: {
kind: "s3" ,
type: "image" ,
bucketName: process . env . S3_BUCKET_NAME ,
region: process . env . S3_REGION ,
// ... S3 configuration
},
} ,
graphql: {
extendGraphqlSchema ,
} ,
ui: {
basePath: "/dashboard" ,
} ,
session: statelessSessions ( sessionConfig ) ,
})
)
) ;
Data Models
Openfront includes 88 KeystoneJS models that power the entire e-commerce platform. Models are defined using Keystone’s declarative schema:
features/keystone/models/Product.ts
import { list } from "@keystone-6/core" ;
import { text , relationship , select , checkbox } from "@keystone-6/core/fields" ;
export const Product = list ({
access: {
operation: {
query : () => true ,
create: permissions . canManageProducts ,
update: permissions . canManageProducts ,
delete: permissions . canManageProducts ,
},
},
fields: {
title: text ({ validation: { isRequired: true } }),
handle: text ({ isIndexed: "unique" }),
description: document ({
formatting: true ,
links: true ,
}),
status: select ({
type: "enum" ,
options: [
{ label: "Draft" , value: "draft" },
{ label: "Published" , value: "published" },
],
defaultValue: "draft" ,
}),
productVariants: relationship ({
ref: "ProductVariant.product" ,
many: true ,
}),
productImages: relationship ({
ref: "ProductImage.products" ,
many: true ,
}),
// ... more fields
},
});
Virtual Fields
Keystone supports virtual fields for computed values that don’t exist in the database:
thumbnail : virtual ({
field: graphql . field ({
type: graphql . String ,
resolve : async ( item , args , context ) => {
const product = await context . query . Product . findOne ({
where: { id: item . id },
query: "productImages(take: 1) { image { url } }" ,
});
return product . productImages [ 0 ]?. image ?. url || null ;
},
}),
})
Virtual fields are powerful for calculating totals, aggregations, and derived data without database migrations.
GraphQL API Layer
Keystone automatically generates a full GraphQL API from your models:
Auto-Generated Queries
# Get single product
query {
product ( where : { id : "..." }) {
id
title
handle
productVariants {
id
title
sku
}
}
}
# List products with filtering
query {
products (
where : { status : { equals : "published" } }
orderBy : { createdAt : desc }
take : 20
) {
id
title
thumbnail
}
}
Auto-Generated Mutations
# Create product
mutation {
createProduct (
data : {
title : "New Product"
status : "draft"
}
) {
id
title
}
}
# Update product
mutation {
updateProduct (
where : { id : "..." }
data : { status : "published" }
) {
id
status
}
}
Custom Mutations
Openfront extends the GraphQL schema with custom business logic in features/keystone/mutations/:
export const extendGraphqlSchema = graphql . extend (( base ) => ({
mutation: {
addToCart: graphql . field ({
type: base . object ( 'Cart' ),
args: {
cartId: graphql . arg ({ type: graphql . ID }),
variantId: graphql . arg ({ type: graphql . nonNull ( graphql . ID ) }),
quantity: graphql . arg ({ type: graphql . Int , defaultValue: 1 }),
},
resolve : async ( source , args , context ) => {
// Custom cart logic
},
}),
},
}));
Access Control & Permissions
Openfront implements a sophisticated role-based access control (RBAC) system:
Permission Definitions
features/keystone/access.ts
export const permissions = {
canAccessDashboard : ({ session }) => {
return !! session ?. data ?. role ?. canAccessDashboard ;
},
canManageProducts : ({ session }) => {
return !! session ?. data ?. role ?. canManageProducts ;
},
canManageOrders : ({ session }) => {
return !! session ?. data ?. role ?. canManageOrders ;
},
// ... more permissions
};
Model-Level Access Control
export const Order = list ({
access: {
operation: {
query: permissions . canManageOrders ,
create: permissions . canManageOrders ,
update: permissions . canManageOrders ,
delete: permissions . canManageOrders ,
},
filter: {
query : ({ session }) => {
// Users can only see their own orders
if ( ! permissions . canManageOrders ({ session })) {
return { user: { id: { equals: session ?. itemId } } };
}
return true ;
},
},
},
// ... fields
});
Session Management
Openfront uses Iron-sealed stateless sessions with cookie-based authentication:
features/keystone/index.ts
export function statelessSessions ({
secret ,
maxAge = 60 * 60 * 24 * 360 , // 360 days
cookieName = "keystonejs-session" ,
}) {
return {
async get ({ context }) {
const cookies = cookie . parse ( context . req . headers . cookie || "" );
const token = cookies [ cookieName ];
if ( ! token ) return ;
return await Iron . unseal ( token , secret , ironOptions );
},
async start ({ context , data }) {
const sealedData = await Iron . seal ( data , secret , {
ttl: maxAge * 1000 ,
});
context . res . setHeader (
"Set-Cookie" ,
cookie . serialize ( cookieName , sealedData , {
maxAge ,
httpOnly: true ,
secure: process . env . NODE_ENV === "production" ,
})
);
return sealedData ;
},
};
}
Storage Architecture
Openfront uses S3-compatible storage for images and files:
storage : {
my_images : {
kind : "s3" ,
type : "image" ,
bucketName : process . env . S3_BUCKET_NAME || "keystone-test" ,
region : process . env . S3_REGION || "ap-southeast-2" ,
accessKeyId : process . env . S3_ACCESS_KEY_ID ,
secretAccessKey : process . env . S3_SECRET_ACCESS_KEY ,
endpoint : process . env . S3_ENDPOINT ,
signed : { expiry : 5000 },
forcePathStyle : true ,
},
}
Production Deployment : Ensure S3 credentials are properly configured and never committed to version control.
Adapter Pattern for Integrations
Openfront uses an adapter pattern for payment and shipping providers, allowing easy swapping of implementations:
Payment Provider Adapter
interface PaymentAdapter {
createPaymentFunction : ( params ) => Promise < PaymentResult >;
capturePaymentFunction : ( params ) => Promise < CaptureResult >;
refundPaymentFunction : ( params ) => Promise < RefundResult >;
getPaymentStatusFunction : ( params ) => Promise < StatusResult >;
handleWebhookFunction : ( params ) => Promise < void >;
}
Shipping Provider Adapter
interface ShippingAdapter {
getRatesFunction : ( params ) => Promise < ShippingRate []>;
createLabelFunction : ( params ) => Promise < ShippingLabel >;
validateAddressFunction : ( params ) => Promise < AddressValidation >;
getTrackingFunction : ( params ) => Promise < TrackingInfo >;
}
Webhook System
Openfront includes a built-in webhook system for event notifications:
features/webhooks/webhook-plugin.ts
export function withWebhooks ( config ) {
return {
... config ,
hooks: {
... config . hooks ,
afterOperation : async ({ operation , item , listKey , context }) => {
// Find matching webhook endpoints
const webhooks = await context . query . WebhookEndpoint . findMany ({
where: {
events: { some: { equals: ` ${ listKey } . ${ operation } ` } },
isActive: { equals: true },
},
});
// Dispatch webhooks
for ( const webhook of webhooks ) {
await fetch ( webhook . url , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ event: operation , listKey , item }),
});
}
},
},
};
}
Database Query Optimization
Indexed fields for fast lookups (isIndexed: "unique")
Efficient relationship queries using query parameter
Virtual fields for computed values without database joins
PostgreSQL-specific optimizations (JSON fields, full-text search)
Server Components for reduced client-side JavaScript
Streaming with Suspense boundaries
Image optimization with next/image
Route-based code splitting
Static generation where possible
Browser caching for static assets
CDN integration for images (S3)
GraphQL query caching
Session caching with stateless cookies
Deployment Architecture
Openfront supports multiple deployment strategies:
Monolith Deployment Single container with both Next.js and Keystone running together. Simplest deployment model.
Microservices Separate Next.js frontend and Keystone backend services for independent scaling.
Environment Configuration
Key environment variables for Openfront:
# Database
DATABASE_URL = postgresql://user:password@localhost:5432/openfront
# Session
SESSION_SECRET = your-32-character-secret-here
# S3 Storage
S3_BUCKET_NAME = openfront-images
S3_REGION = us-east-1
S3_ACCESS_KEY_ID = your-access-key
S3_SECRET_ACCESS_KEY = your-secret-key
S3_ENDPOINT = https://s3.amazonaws.com
# Payment Providers
STRIPE_SECRET_KEY = sk_test_...
PAYPAL_CLIENT_ID = ...
PAYPAL_CLIENT_SECRET = ...
# Shipping Providers
SHIPPO_API_KEY = ...
SHIPENGINE_API_KEY = ...
Next Steps
Data Models Explore the 88+ KeystoneJS models that power Openfront
Authentication Learn about session-based auth, OAuth, and API keys
GraphQL API Dive into the GraphQL API reference
Deployment Deploy Openfront to production