Skip to main content

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 }),
          });
        }
      },
    },
  };
}

Performance Optimizations

  • 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:
.env
# 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

Build docs developers (and LLMs) love