# GraphQL API
Openship exposes Keystone GraphQL at `/api/graphql`. The generated `schema.graphql` includes list operations for shops, channels, orders, items, matches, links and tracking, plus custom product, order, webhook and purchase operations.
Do not send production shop/channel credentials or API keys to a public demo playground. Inspect the generated schema locally or use an authorized non-production deployment.
Authentication [#authentication]
Dashboard sessions and bearer API keys are present in current source. API keys have hashed secret material, scopes, status, expiry and usage fields. Verify that every operation you expose checks the expected scope and row ownership; a stored scope is not enforcement by itself.
Example: search a shop catalog [#example-search-a-shop-catalog]
The current generated contract accepts `shopId` and `searchEntry` and returns a provider-neutral `ShopProduct`:
```graphql
query SearchShopProducts($shopId: ID!, $search: String) {
searchShopProducts(shopId: $shopId, searchEntry: $search) {
productId
variantId
title
image
price
inventory
inventoryTracked
availableForSale
productLink
error
}
}
```
Core custom operations [#core-custom-operations]
Current generated names include:
* `searchShopProducts`, `getShopProduct`, and `searchShopOrders`;
* `searchChannelProducts` and `getChannelProduct`;
* `getShopWebhooks`, `createShopWebhook`, and `deleteShopWebhook`;
* `getChannelWebhooks`, `createChannelWebhook`, and `deleteChannelWebhook`;
* `createChannelPurchase`, `cancelPurchase`, and `cancelOrder`;
* `getMatch`, `getMatchCount`, `upsertMatch`, `overwriteMatch`, and `matchOrder`.
Read exact inputs and result types from `schema.graphql`. Handler support differs by platform. A generated operation can exist while one adapter returns an error or lacks a production implementation.
Safety checks [#safety-checks]
Before external access, test missing/expired/wrong-scope keys, cross-user IDs, arbitrary endpoint input, provider timeouts, duplicate purchase retries, webhook signature/replay, redacted errors and cancellation after partial fulfillment.
# Channels
A **Channel** is one user-owned fulfillment destination. A **ChannelPlatform** describes how Openship searches the destination catalog, retrieves one item, creates or cancels a downstream purchase, registers provider webhooks, and interprets tracking or cancellation events.
Current source contains compiled channel adapters for Shopify and Openfront only. A supplier, warehouse, 3PL, marketplace, WooCommerce, or BigCommerce connection requires a separately implemented and verified adapter or custom endpoint. A configured ChannelPlatform does not prove that an external purchase, cancellation, or tracking update occurred.
Current model boundary [#current-model-boundary]
`Channel` belongs directly to one Openship user and references one `ChannelPlatform`. It holds destination identity, domain, access/refresh token fields, expiry, metadata, channel items, links, and cart items. `ChannelPlatform` stores operation selectors and OAuth configuration.
`Match` connects a shop item/variant to one channel item/variant. `CartItem` records the selected downstream line, and `TrackingDetail` records tracking facts propagated through the routing path. The upstream shop and downstream provider remain authoritative for their own inventory, orders, funds, and shipment state.
Channel ownership is user-scoped. Every operation must also validate ownership of the linked shop, order, match, cart item, and channel item; selecting a related ID must not move data across users.
Compiled adapters and custom execution [#compiled-adapters-and-custom-execution]
Current files under `features/integrations/channel` provide Shopify and Openfront handlers plus an executor. The operation family includes channel product search/detail, purchase creation/cancellation, webhook management, and tracking interpretation.
The executor can also call a database-selected HTTP URL or dynamic function path. Treat that as privileged code/network configuration. Restrict destinations, prevent private-address and redirect escapes, cap time/body size, validate request and response schemas, isolate credentials, and use stable provider idempotency keys. The current configurable path is not a general secure 3PL connector by itself.
Routing workflow [#routing-workflow]
1. Configure a synthetic Shop and ShopPlatform.
2. Open `/dashboard/platform/channels` and configure a ChannelPlatform tied to an implemented test adapter.
3. Create a user-owned Channel with sandbox credentials.
4. Use channel product search/detail to identify a candidate variant.
5. Open `/dashboard/platform/matches` and match the shop line to that exact channel item.
6. Link the shop and channel, then route a synthetic order through `matchOrder`, cart commands, and `createChannelPurchase`/`placeOrders` as required by the current schema.
7. Record downstream purchase identity and test tracking/cancellation callbacks.
8. Reconcile Openship, shop, and channel state after success, duplicate delivery, timeout, partial failure, and cancellation.
There is no current channel-onboarding mutation or demo seed. The docs site's POST-only demo endpoints return in-memory synthetic payloads for adapter development; they are not a fulfillment provider, durable order store, invoice service, or live 3PL.
Controlled GraphQL boundary [#controlled-graphql-boundary]
Current custom operations include `searchChannelProducts`, `getChannelProduct`, `createChannelPurchase`, `cancelPurchase`, channel webhook management, matching queries/commands, `addToCart`, `addMatchToCart`, and `placeOrders`. Handler support differs by adapter, so inspect `schema.graphql` and the selected implementation together.
Do not use generated CRUD to mark a purchase fulfilled or tracking complete when an external effect has not been authenticated and reconciled. Preserve the provider event ID, route ownership, bounded status/error evidence, and immutable shop/channel item identities.
Current limitations [#current-limitations]
Current source does not establish WooCommerce, BigCommerce, Amazon, email, spreadsheet, generic 3PL, or carrier integrations. It does not provide payment settlement, warehouse inventory authority, durable retry for every webhook path, or proof that arbitrary configured HTTP endpoints are safe. Channel credential fields also require a deployment-specific encryption and field-access review.
One current cancellation handler queries a `CartItem.title` field while the registered model defines `CartItem.name`; verify and repair that route in the owning Openship source before relying on channel cancellation. Also test webhook raw-body signatures, replay IDs, asynchronous failure persistence, token redaction, cross-user IDs, duplicate purchases, cancellation after partial fulfillment, and tracking reconciliation.
See [Product matching](/docs/openship/ecommerce/product-matching) and [Create a custom channel integration](/docs/openship/ecommerce/how-to-guides/create-custom-channel).
# Comparisons
Openship is a self-hosted order-routing application. It stores shops, channels, links, matches, orders, and downstream purchase state, then invokes configured adapter operations to read source orders and place fulfillment purchases.
Its main distinction from a hosted order-management service is ownership: you operate the source, database, deployment, credentials, adapter destinations, retries, and reconciliation.
Compare operating models [#compare-operating-models]
| Approach | Integration boundary | Operations responsibility | Typical tradeoff |
| -------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Openship | Built-in or configured shop/channel adapter operations | The operator owns deployment, credentials, routing behavior, and recovery | Source-level control with corresponding engineering and operational work |
| Hosted order-management service | Vendor connectors, rules, webhooks, and APIs | The vendor operates the core service; the customer configures it | Less core infrastructure work, within the vendor's connector and rule model |
| Marketplace or store routing app | One platform's extension surface | Shared between the app vendor and merchant | Faster fit for supported platform workflows, with a narrower platform boundary |
| Custom routing service | Contracts and workflows designed by the implementing team | Entirely owned by that team | Maximum freedom without Openship's existing models, dashboard, and adapter executors |
Compare exact connectors, cancellation and fulfillment behavior, retry semantics, audit evidence, support, and total operating cost—not category labels alone.
Current Openship integration scope [#current-openship-integration-scope]
Current compiled source includes Shopify and Openfront handlers on both shop and channel sides. Platform records can also configure operation destinations for independent HTTP route apps or local modules, subject to the executor's contract.
That architecture is extensible, but it is not the same as verified compatibility with every commerce or fulfillment system. Each additional platform needs implementations for the operations it uses, authentication and credential handling, payload validation, webhook verification, idempotency, timeout and retry policy, and reconciliation tests.
Routing behavior [#routing-behavior]
Openship can:
* record source orders and their line items;
* select configured shop-to-channel links using link filters and rank;
* use saved item matches to build downstream cart items;
* call channel adapters to create purchases;
* retain status and error information for operator review.
Delivery latency depends on webhook or polling behavior, queueing, provider availability, and the configured execution path. Current source does not establish a universal real-time delivery guarantee.
Cost and setup [#cost-and-setup]
Open source does not mean zero operating cost or setup in a fixed number of minutes. A useful evaluation needs a database, deployment, operator account, credentials, shop/channel records, links or matches, webhook configuration, and a tested failure-and-reconciliation path.
Infrastructure, provider, marketplace, support, monitoring, and implementation charges remain deployment-specific.
When Openship may fit [#when-openship-may-fit]
Consider Openship when a team:
* wants to own and modify its order-routing source and data;
* has a supported adapter or is prepared to implement and test one;
* needs explicit shop, channel, link, match, and downstream purchase records;
* can operate retries, reconciliation, credential rotation, and webhook verification.
A hosted or platform-specific alternative may fit better when a required connector, service-level agreement, support program, reporting suite, or compliance program already exists there and source-level customization is not required.
# Deployment
Openship needs a Node.js runtime and persistent PostgreSQL. Each enabled shop or channel integration adds its own credential, network, webhook and operational requirements.
Build and migration warning [#build-and-migration-warning]
The current `npm run build` script generates the Keystone schema, deploys Prisma migrations, and builds Next.js. Use one reviewed release job for migrations rather than allowing multiple build workers to race against production.
Release checklist [#release-checklist]
1. Pin the source revision and install from the lockfile.
2. Generate the schema and run current static checks.
3. Back up the target and test migrations on a realistic disposable clone.
4. Store `DATABASE_URL`, `SESSION_SECRET` and integration credentials in the host's secret manager.
5. Apply migrations once, deploy the app, and run session/API-key ownership negatives.
6. Route a synthetic order through every enabled shop/channel pair.
7. Test duplicate delivery, timeout, retry, partial failure, cancellation, fulfillment and tracking updates.
8. Verify raw webhook signatures and replay IDs.
9. Monitor stuck orders and keep an operator reconciliation path.
Railway, Vercel, Render or other templates can create infrastructure. They do not verify database sizing, migration history, provider reachability, background work, retries, webhooks, backups or incident response. A successful deploy is not an order-routing readiness claim.
# Getting started
Prerequisites [#prerequisites]
* Node.js 20 or newer
* PostgreSQL
* Git and npm
* an empty, isolated database
Clone and install [#clone-and-install]
```bash
git clone https://github.com/openshiporg/openship.git
cd openship
npm install
```
Configure the local environment [#configure-the-local-environment]
Copy the repository's `.env.example` when present and set at least:
```bash
DATABASE_URL="postgresql://username:password@localhost:5432/openship"
SESSION_SECRET="replace-with-a-random-string-at-least-32-characters"
```
Add only the SMTP, AI, shop or channel secrets required by the handler you intend to test. Keep credentials out of source control.
Review migrations and start [#review-migrations-and-start]
```bash
npm run dev
```
The current script generates the Keystone schema, runs `prisma migrate deploy`, and starts Next.js. Confirm the database target and migration history before running it.
Create the first dashboard user [#create-the-first-dashboard-user]
Open `/dashboard/init`, create the first operator, and sign in at `/dashboard`. Current source has no Openship onboarding mutation or seed, so platform, shop, channel, link, and match records are configured manually. Do not copy credentials from a public example.
Build one synthetic route [#build-one-synthetic-route]
Create one test shop, one test channel, a link, and one exact variant match. Current compiled handlers cover Shopify and Openfront; the docs site's POST-only demo endpoints can exercise custom-handler shapes with in-memory synthetic data. Route a synthetic order and inspect both Openship and the downstream test system.
Evaluation checks [#evaluation-checks]
* API keys and sessions cannot read another user's shops, channels, matches or orders.
* Shop and Channel credential fields are denied or encrypted before real secrets are stored.
* Database-selected HTTP/function destinations are constrained to reviewed allowlists and cannot reach private or metadata networks through DNS or redirects.
* retries do not create duplicate downstream purchases.
* webhook signatures and event IDs are checked before state changes.
* a partial channel failure remains visible and can be retried or reconciled.
`npm run build` currently deploys migrations before building, the checked-in `lint` script uses the removed Next.js 16 `next lint` command, and the package has no test or typecheck script. Do not represent a release as fully verified or connect a production shop/supplier until the owning source adds current gates and the exact handler passes product, order, ownership, egress, credential, cancellation, fulfillment, tracking, replay, retry, and reconciliation tests.
# Openship order routing
[Source](https://github.com/openshiporg/openship) · [Getting started](/docs/openship/ecommerce/getting-started)
Openship connects the places where orders originate with the places that fulfill them. A **shop** is an order source. A **channel** is a fulfillment destination. **Links** connect shops to channels, and **matches** map a shop item to a channel item before an order is placed downstream.
The repository contains shop-to-channel routing and Shopify/Openfront adapters. A configured shop/channel, provider name, model, or synthetic endpoint response does not prove an external purchase, webhook, retry, cancellation, or tracking flow. Current credential fields, configurable execution destinations, and non-durable callback paths require hardening before production credentials or orders.
Architecture and data [#architecture-and-data]
Openship uses the same Next.js, Keystone, GraphQL, Prisma and PostgreSQL application base as Openfront. Its active graph is smaller and routing-specific: users/roles/API keys, shop and channel platforms, shops and channels, orders/line/cart items, shop/channel items, matches, links and tracking details. Ownership is directly User-scoped; there is no organization or workspace tenant graph.
Provider-specific behavior belongs behind shop and channel platform handlers. Openship stores its routing and match state; connected commerce or fulfillment platforms remain authoritative for their own products, orders and tracking records.
Main workflow [#main-workflow]
1. Configure a shop platform and shop with scoped credentials.
2. Configure a channel platform and fulfillment channel.
3. Link the shop to the channel.
4. Search both product catalogs and create a match.
5. Import or select a shop order.
6. Convert matched lines into channel purchases.
7. record tracking, cancellation, fulfillment or error state from the channel boundary.
Idempotency, signature verification, credential isolation, retries and reconciliation need to be proved for each adapter.
Openfront relationship [#openfront-relationship]
Openfront products can be shop sources for Openship, but Openship is not the Openfront storefront or payment system. It coordinates order routing across independently operated systems.
# Product matching
A `Match` records how one or more source `ShopItem` records map to one or more destination `ChannelItem` records. Openship uses these saved mappings when an order is configured with `matchOrder`.
Match records [#match-records]
A match has:
* `input`: the shop-side product ID, variant ID, quantity, shop, and owning user;
* `output`: the channel-side product ID, variant ID, quantity, saved price, channel, and owning user;
* an owning user used by access filters and order matching.
The relationships are many-to-many. A one-input, one-output match covers a direct variant mapping; one input with several outputs can represent a bundle.
Create a match [#create-a-match]
Connect a shop and channel [#connect-a-shop-and-channel]
Configure credentials and verify that both adapter search operations work. Current compiled handlers cover Shopify and Openfront. Other platforms require compatible configured operations.
Search each platform [#search-each-platform]
The Matches workspace calls the selected shop and channel adapters to search products on demand. This is not a background import of either complete catalog.
Select exact items [#select-exact-items]
Choose the source and destination product/variant identities and quantities. Current source does not provide an automatic SKU-matching workflow, so confirm each mapping explicitly.
Save and test [#save-and-test]
Creating a `Match` reuses or creates the corresponding `ShopItem` and `ChannelItem` records. Route a synthetic order and inspect the generated cart items before enabling downstream purchase creation.
How an order uses matches [#how-an-order-uses-matches]
When a new order has `matchOrder` enabled and does not take the separate link-routing path, current source:
1. loads the order and its line items;
2. searches matches owned by the same user using product ID, variant ID, and quantity;
3. first looks for a combined match covering all order lines, then tries individual line matches;
4. reads each matched destination product through its channel adapter;
5. creates downstream `CartItem` records from the saved outputs;
6. records a price-change error when the current destination price differs from the saved match price;
7. optionally calls downstream purchase placement when `processOrder` is enabled.
If no suitable match is found, Openship records a match error and leaves the order for operator handling rather than proving successful fulfillment.
Matching is exact application logic, not probabilistic product identification. Product IDs, variant IDs, quantities, ownership, adapter responses, and current prices must all be tested. Retries and downstream purchase creation also need idempotency and reconciliation coverage before live routing.
Current boundaries [#current-boundaries]
* There is no automatic full-catalog import in the current Matches workflow.
* There is no implemented automatic SKU-matching action.
* Availability- or destination-based selection among several channels is not implemented by the Match lookup.
* Inventory synchronization is eligible only for one-input/one-output matches where both quantities are `1` and both adapters expose inventory values.
* No checked-in benchmark establishes a supported catalog size or routing throughput.
* A saved match does not guarantee that a destination item is still available, unchanged, or purchasable; the channel response and resulting errors still require review.
# Schema Visualizer
Database Schema [#database-schema]
Explore the complete Openship database schema with this interactive visualization. Click and drag to navigate, zoom to focus on specific areas, and see how all the models connect together.
Understanding the Schema [#understanding-the-schema]
The Openship database is built using Prisma with PostgreSQL and consists of several core model groups:
User Management [#user-management]
* **User** - The central user model with role-based permissions
* **Role** - Defines what actions users can perform
* **ApiKey** - API access tokens for programmatic access
Platform Architecture [#platform-architecture]
* **ShopPlatform** - Templates for shop integrations (e.g., Shopify, WooCommerce)
* **ChannelPlatform** - Templates for channel integrations (e.g., suppliers, 3PLs)
* **Shop** - Individual shop instances using platform templates
* **Channel** - Individual channel instances using platform templates
Order Management [#order-management]
* **Order** - Customer orders from shops
* **LineItem** - Individual items within orders
* **CartItem** - Items being purchased from channels
* **TrackingDetail** - Shipping and fulfillment tracking
Product Matching [#product-matching]
* **ShopItem** - Products available from shops
* **ChannelItem** - Products available from channels
* **Match** - Connections between shop and channel products
* **Link** - Automated routing rules between shops and channels
Key Relationships [#key-relationships]
The schema shows how Openship creates a bridge between e-commerce platforms:
1. **Platform → Instance**: Platforms define how to integrate, while Shops/Channels are configured instances
2. **Order Flow**: Orders → LineItems → CartItems (automated purchasing)
3. **Product Matching**: ShopItems ↔ Matches ↔ ChannelItems (product mapping)
4. **Fulfillment**: CartItems → TrackingDetails (shipping updates)
This architecture allows Openship to automatically route orders from any connected shop to any connected channel based on your configured matches and links.
# Shops
A **Shop** is one user-owned order source. A **ShopPlatform** describes how Openship searches that source's products and orders, retrieves one product, updates product data, and manages source webhooks. The platform is reusable configuration; the shop holds the connection-specific domain and credentials.
Current source contains compiled shop adapters for Shopify and Openfront only. BigCommerce, WooCommerce, Amazon, eBay, email, and spreadsheet names are not built-in shop adapters. A custom HTTP endpoint can be configured, but that does not make it trusted, compatible, or retry-safe.
Current model boundary [#current-model-boundary]
`Shop` belongs directly to one Openship user and references one `ShopPlatform`. It stores the source name, domain, access/refresh token fields, token expiry, metadata, orders, links, and shop items. `ShopPlatform` stores the operation selectors and OAuth configuration used by its shops.
Openship is user-scoped rather than organization- or workspace-scoped. Every query and command must verify that the acting session or API key owns the selected shop and any related order, link, match, or item. Regions, channels, and provider domains do not create a separate tenant boundary.
The current token fields are ordinary model text fields and do not establish encrypted secret storage or field-read denial. Treat the source as requiring a credential-storage review before connecting a real store.
Compiled adapters [#compiled-adapters]
Current files under `features/integrations/shop` provide:
* `shopify.ts` for Shopify product/order operations;
* `openfront.ts` for the Openfront GraphQL contract;
* `lib/executor.ts` for dispatching the selected operation.
The adapter shape includes product search/detail, order search, product update, and webhook operations. Handler support varies; an operation appearing in GraphQL or a platform row does not prove that both compiled adapters implement the same effect.
The executor also accepts database-selected HTTP URLs and dynamic function paths. That is an extension mechanism, not an allowlist. Before enabling custom execution, restrict schemes and destinations, block private/network metadata addresses, disable redirects, bound time and response size, validate response schemas, isolate credentials, and record retries and reconciliation.
Operator workflow [#operator-workflow]
1. Initialize the first user at `/dashboard/init` and sign in.
2. Open `/dashboard/platform/shops`.
3. Create or inspect a `ShopPlatform` whose operation selectors match an implemented adapter.
4. Create a Shop owned by the current user and attach only synthetic/sandbox credentials.
5. Run `searchShopProducts`, `getShopProduct`, and `searchShopOrders` against test data.
6. Create a Link to a test channel and Match shop variants to channel variants.
7. Exercise webhook creation/deletion and inbound order handling with duplicate, invalid-signature, and wrong-user cases.
There is no current first-shop onboarding mutation or demo seed. Platform and shop records are configured manually after first-user initialization.
Bounded GraphQL operations [#bounded-graphql-operations]
The current custom schema includes `searchShopProducts`, `getShopProduct`, `searchShopOrders`, shop webhook queries/mutations, product updates, matching, cart, and order-routing commands. Read the generated `schema.graphql` from the exact source revision for inputs and result types.
Use those operations only through ownership-checked server paths. Do not expose raw Shop/ShopPlatform credential fields or treat API-key scope strings as enforcement without testing every resolver and related record.
Current limitations [#current-limitations]
Openship does not provide a customer storefront, payment checkout, product-authority database, or general ecommerce tenant. Current source has no onboarding seed, no complete provider conformance suite, and no durable delivery ledger for every inbound/outbound webhook path. It also permits configurable execution destinations and stores shop credentials in fields that need additional protection.
Before a real source connection, prove token secrecy and rotation, API-key and cross-user denial, OAuth state/callback ownership, signed raw-body webhooks, event replay handling, bounded egress, provider timeout behavior, order idempotency, cancellation after partial routing, and reconciliation against the source shop.
See [GraphQL API](/docs/openship/ecommerce/api-reference) and [Create a custom shop integration](/docs/openship/ecommerce/how-to-guides/create-custom-shop).
# What is Openship?
Openship is an operator order router. It connects a **shop**, where an order originates, to a **channel**, where matched items are purchased for fulfillment. It stores routing, match, purchase-reference, and tracking state; connected systems remain authoritative for their own catalog, inventory, customer order, funds, and shipment facts.
Current compiled adapters cover Shopify and Openfront only. Other commerce, marketplace, warehouse, 3PL, email, or spreadsheet systems require separate adapter source and verification. Openship is not a customer storefront, payment processor, warehouse system, or shipping carrier.
Shops [#shops]
A Shop is one user-owned order source. Its ShopPlatform selects product, order, update, and webhook handlers. The shop holds connection-specific identity and credentials.
See [Shops](/docs/openship/ecommerce/shops) for the current adapter and credential limitations.
Channels [#channels]
A Channel is one user-owned fulfillment destination. Its ChannelPlatform selects catalog, purchase, cancellation, webhook, and tracking handlers. Creating a Channel row does not establish a working provider connection.
See [Channels](/docs/openship/ecommerce/channels) for compiled adapters, custom execution, and reconciliation requirements.
Links [#links]
A Link connects one owned shop to one owned channel and supplies the routing relationship used for order work.
Matches [#matches]
A Match maps one exact shop product/variant to one exact channel product/variant. Routing uses that mapping to decide which destination item can fulfill a source line. Operators can replace a match without changing the source catalog, but historical order/purchase identities should not drift when the current mapping changes.
Controlled flow [#controlled-flow]
The intended path is shop order -> line match -> channel cart item -> downstream purchase -> authenticated tracking/cancellation -> source update. Custom GraphQL operations handle product/order search, matching, cart construction, purchase placement/cancellation, and webhooks. Each step needs same-user ownership, adapter allowlisting, idempotency, signed callback verification, durable error evidence, and reconciliation.
Current source has no onboarding seed. Initialize the first user at `/dashboard/init`, then configure synthetic platforms, one shop, one channel, a link, and matches manually. Do not connect production credentials until the exact adapter passes cross-user, credential-read, egress, duplicate-purchase, partial-failure, cancellation, tracking, and restore tests.
# External dashboards
To create a custom Airline dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Airline's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Airline codebase before proposing work.
# External storefronts
To create a custom Airline storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Airline's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Airline codebase before proposing work.
# Openfront Airline
[Source](https://github.com/openshiporg/openfront-airline) · [Catalog](https://openship.org/products/openfront-airline)
Openfront Airline is an open-source airline retailing and passenger-service backend. It models direct and agent distribution, offers, bookings, fulfillment documents, payments, airport passenger service, baggage, and disruption recovery under carrier-owned data and permissions.
This is not flight operations or safety-critical aviation software. It does not calculate flight plans, dispatch releases, fuel, weight and balance, airworthiness, crew legality, ATC decisions, or operational control.
Install and configure [#install-and-configure]
Use Node.js 20.9 or later and PostgreSQL. From the repository root:
```bash
npm ci
```
Create `.env` for the intended local database and session:
```bash
DATABASE_URL=postgresql://user:password@127.0.0.1:5432/openfront_airline
SESSION_SECRET=replace-with-at-least-32-random-characters
INITIAL_CARRIER_NAME=Openfront Airline
INITIAL_CARRIER_LEGAL_NAME=Openfront Airline
INITIAL_CARRIER_CODE=OF
INITIAL_CURRENCY_CODE=USD
INITIAL_TIMEZONE=UTC
```
A production deployment also needs `APP_BASE_URL` set to one trusted origin such as `https://airline.example`. It must not contain a path, credentials, query, or fragment. Internal GraphQL and MCP requests ignore request `Host` and forwarded-host headers.
Review and apply checked-in migrations, then start development:
```bash
npm run migrate
npm run dev
```
`npm run dev` also deploys migrations, so use it only with the database you intend to update. The app is served at `/`, the generated operator dashboard at `/dashboard`, and GraphQL at `/api/graphql`. Do not use schema push or destructive reset commands.
Run source checks with:
```bash
npm run lint
npm run typecheck
npm test
npm run build
```
`npm run build` generates Keystone schemas and builds Next.js without applying migrations.
Architecture [#architecture]
`Carrier` is the tenant root. Carrier-owned records carry a direct carrier relationship, and permissions are split by network, inventory, offers, bookings, passengers, ticketing, payments, airport service, disruptions, integrations, users, roles, and audit. `Airport` is shared reference data.
Current source registers 48 Keystone and Prisma models. Accepted offers, booking lines, booking segments, tickets, and financial records snapshot immutable commercial and service facts. Money uses integer minor units and explicit currency. Lifecycle and idempotency helpers control booking and passenger-service transitions.
The custom GraphQL boundary now exposes the bounded `airlineFlightOptions` projection plus eight commands: `createOfferFromShopping`, `createBookingFromOffer`, `captureBookingPayment`, `issueBookingTickets`, `checkInBookingPassenger`, `acceptCheckedBag`, `transitionBookingStatus`, and `transitionPassengerService`. Raw model CRUD remains available only where carrier access and field policy permit it; task workspaces call these carrier-scoped commands rather than assembling commercial state in the browser.
Data model [#data-model]
* **Network and schedule:** airports, carriers, routes, recurring schedules, dated flight instances, cabins, seat maps, and seats.
* **Inventory and retailing:** inventory buckets and holds, fare brands and rules, ancillary products, offers, and immutable offer items.
* **Orders and passengers:** passengers, protected travel-document references, bookings and PNR locators, booking passengers, journeys, segments, and immutable booking lines.
* **Fulfillment:** tickets and coupons, electronic miscellaneous documents and coupons, seat assignments, and service requests.
* **Airport service:** check-in records, boarding passes, bags and bag events.
* **Disruption:** disruptions, reaccommodation records, exchanges, and affected booking/service relationships.
* **Finance and integrations:** payment sessions, payments, refunds, exchanges, payment providers, integration providers, provider connections, webhooks, approved agents, idempotency keys, and audit events.
Airline nouns remain explicit. Offers and bookings are not renamed ecommerce products and orders, and passenger-service documents are not generic fulfillment JSON.
Workflows [#workflows]
The commercial path is schedule and inventory -> priced offer with expiring hold -> booking/PNR with immutable lines -> payment and ticket/EMD fulfillment -> check-in, seat, boarding pass, and baggage service -> disruption and reaccommodation when required.
`createOfferFromShopping` creates an expiring hold from server-read inventory. `createBookingFromOffer` accepts that offer and consumes inventory inside the bounded transaction. The simulated demo path then captures payment, issues tickets/coupons, checks in one booking passenger, and accepts a bag. `transitionBookingStatus` and `transitionPassengerService` enforce allowed state changes, carrier scope, request-bound idempotency, actor evidence, and terminal-state protection.
Current first-user initialization can create an administrator and carrier from `INITIAL_*` values without fabricating network or commercial data. Separately, `npm run seed:demo` creates a fictional carrier/operator, OFR-DMI flight, fare, inventory bucket, seats, passenger, and simulated payment provider only when `ALLOW_DEMO_SEED=1` and `DATABASE_URL` points to loopback PostgreSQL. The home page and authenticated Operations, Shop & hold, Booking desk, and Departure control workspaces exercise that synthetic path; generic model administration remains available. There is no public traveler booking storefront, live provider connection, or hosted demo URL advertised by these docs.
Integrations [#integrations]
The model defines boundaries for GDS, NDC, DCS, payment, identity, baggage, provider connections, webhooks, and appointed agents. Current source does not include live provider adapters or signed inbound webhook routes. A provider record or endpoint row does not establish a working airline connection.
Each adapter needs exact carrier scope, typed request and result contracts, credential isolation, provider idempotency, signed ingress and replay protection, bounded payload evidence, retries, reconciliation, observability, and an operator exception path. Inventory and booking adapters must also preserve the local hold and snapshot contracts instead of allowing provider responses to rewrite accepted commercial history.
Security [#security]
Carrier filters and field access restrict cross-carrier reads and writes. Permission grants cannot exceed the acting user's grant authority. Controlled operations recheck carrier ownership and bind idempotency to tenant, operation, target, and request content.
Travel documents use an encrypted or tokenized payload contract, masked metadata, and one-way lookup hashes. Payment records store provider references and masked instruments rather than PAN or CVV. Sensitive fields and audit metadata are bounded and hidden from ordinary output. Completion/MCP cookie forwarding is request-local, exact-origin and exact-path, rejects redirects, and does not patch process-global fetch.
Before real passenger data, complete the vault/encryption adapter, key rotation, retention and redaction jobs, signed provider ingress, cross-carrier runtime tests, rate and body limits, monitoring, backup restoration, incident response, and applicable identity, payment, privacy, consumer, accessibility, and aviation-service requirements.
Deployment [#deployment]
Current source contains eight checked-in migration directories, including the airline domain migration. `npm run build` generates Keystone artifacts and builds Next.js without applying migrations; `npm run migrate` deploys the checked history, while `npm run dev` applies it before starting development. Review and back up the target, migrate as a separate controlled deployment step, configure stable secrets and one trusted `APP_BASE_URL`, then run unit/schema checks, `npm run test:integration`, authenticated task-workspace checks, and rollback/restoration exercises against the exact release.
The source contains a serializable inventory transaction helper and concurrent offer/booking tests, but that does not prove capacity behavior under a deployment's real load, timeout, retry, and provider conditions. It also does not supply real encryption/key custody, GDS/NDC/DCS/payment/identity/baggage adapters, signed ingress, reconciliation, production observability, or a public traveler product. Do not accept real passenger or payment data until those boundaries and applicable operational requirements have been independently implemented and tested.
Extension paths [#extension-paths]
* Add domain lists under `features/keystone/models` and register them explicitly in `features/keystone/models/index.ts`; preserve direct carrier ownership and immutable accepted snapshots.
* Add commercial or passenger-service state changes under `features/keystone/mutations` using carrier checks, lifecycle adjacency, idempotency, transaction boundaries, and audit evidence. Do not make protected states freely writable.
* Put GDS, NDC, DCS, payment, identity, baggage, and notification adapters behind typed provider modules. Keep secrets out of GraphQL, verify inbound signatures, claim event IDs, and reconcile external and local state.
* Build traveler and agent storefront routes separately from authenticated reservation, airport, and disruption workspaces. Call narrow operations rather than reproducing offer acceptance or transition policy in the browser.
* Extend initial carrier setup without turning environment defaults into operational network, fare, or inventory truth. Seed only synthetic evaluation data and keep production reference data operator-owned.
* Add tests for cross-carrier access, hold expiry, concurrent inventory consumption, immutable snapshots, terminal transitions, idempotent replay, credential denial, duplicate callbacks, provider partial failure, and disruption servicing before enabling a new path.
# External dashboards
To create a custom Barbershop dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Barbershop's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Barbershop codebase before proposing work.
# External storefronts
To create a custom Barbershop storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Barbershop's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Barbershop codebase before proposing work.
# Openfront Barbershop
[Source](https://github.com/openshiporg/openfront-barbershop) · [Catalog](https://openship.org/products/openfront-barbershop)
Openfront Barbershop is a service-booking branch with public services and barber profiles, scheduled appointments, walk-in queues, customer history, packages/memberships, POS/manual payments, commissions, inventory, and reports.
Current source provides shop-scoped booking, queue, checkout, inventory, commission, and customer workflows. The only registered payment adapter is manual and has no public webhook behavior; the presence of a webhook route does not establish card settlement.
Architecture and schema [#architecture-and-schema]
`Shop` is the tenant root. `ShopMembership`, active-shop selection, and managed-shop checks scope appointments, services, staff, customers, transactions, payment sessions, inventory, commissions, and queue records. The graph includes shop settings, service categories/services, barbers, chairs and schedules, customers/notes/grooming notes, appointments and immutable line items, queue/waitlist records, packages/memberships, products/lots/suppliers, transactions, tips/commissions, waivers, payment providers/sessions, and webhook events.
Public routes cover `/services`, `/barbers`, `/book`, and `/appointment`. Operator routes under `/dashboard/platform` cover booking, customers, POS, queue, reports, services, staff, and barbershop operations.
Main workflow [#main-workflow]
The shop publishes services, barber availability and policies. A guest books a service or joins the walk-in flow. Staff assign the work, check the client in, complete service and checkout, then record tips, commission and customer history. Appointment lines keep the accepted service and price context.
Bounded GraphQL operations [#bounded-graphql-operations]
Current platform queries provide booking and customer workspaces. Named operations add customer notes; set customer state; mark no-shows; record and return product sales; transition and pay approved commissions; reconcile payment sessions; select the active shop; and drive booking, queue, checkout, and membership work through scoped actions. Generated CRUD remains for permitted configuration, but appointment, stock, payment, and commission transitions need the named transaction and evidence paths.
Setup and onboarding [#setup-and-onboarding]
Use a disposable PostgreSQL database. `runBarbershopOnboarding` invokes a dependency-ordered custom seed runner for synthetic Stack & Fade shop, services, barbers/schedules, customers, packages/memberships, appointments, queue, retail inventory, transactions, and manual payment records. It does not use the canonical shared hook filename and is not a hosted demo. Run it twice on an isolated database, then test active-shop selection, cross-shop relations, guest-token ownership, queue/appointment transitions, commission and inventory evidence, and concurrent slot requests.
Integrations [#integrations]
The current registered payment shape is manual-only. The manual adapter is operator/offline-oriented and does not register webhooks. Although `/api/payment-webhooks/[providerCode]` exists, the domain utility rejects providers without an allowlisted public-settlement webhook adapter. No Stripe or PayPal adapter is current. A public flow must never mark itself paid from the existence of a session or route.
Security and deployment [#security-and-deployment]
Appointment and queue lookup should use opaque hashed tokens. Booking capacity needs database serialization. Customer notes, waivers, payment records and provider credentials need narrow access. Railway configuration builds then migrates at start, but does not prove deployment.
Deploy only after current schema/type/tests, reviewed migrations, cross-shop and active-membership negatives, concurrent booking/queue, public-token, manual-settlement, stock, commission, restore, and responsive operator/customer checks pass. Current source does not establish external card processing, barber licensing, messaging, payroll, tax, accessibility, privacy, or operating certification.
# External dashboards
To create a custom Coffee Shop dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Coffee Shop's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Coffee Shop codebase before proposing work.
# External storefronts
To create a custom Coffee Shop storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Coffee Shop's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Coffee Shop codebase before proposing work.
# Openfront Coffee Shop
[Source](https://github.com/openshiporg/openfront-coffeeshop) · [Catalog](https://openship.org/products/openfront-coffee-shop)
Openfront Coffee Shop covers a cafe's menu, pickup ordering, counter/POS work, ingredients and recipes, inventory, loyalty, subscriptions, catering, shifts, purchasing, prep and waste.
Manual/pay-at-counter is the seeded payment scope. Stripe and PayPal adapters make provider API calls and verify webhooks, but onboarding leaves them disabled; adapter source is not evidence of enabled checkout, settlement, refund, or reconciliation.
Architecture and schema [#architecture-and-schema]
`StoreSettings` is the single-store operating root; current source is not a multi-cafe tenant graph. The graph includes store settings, menu categories/items/images/modifiers, cafe/POS/catering orders and immutable order lines, payment/provider/event records, ingredients, recipes, inventory items/lots, movements, suppliers/purchase orders, prep batches, waste, loyalty accounts/events, subscriptions and shifts.
The public site has `/`, menu detail, `/checkout`, and `/order-confirmed`. Operator routes cover menu, orders, POS, KDS, reports, and coffee operations. Current source also contains register-shift and staff-shift operations; those are active records/workflows, not just future schema.
Main workflow [#main-workflow]
A guest builds a pickup order from current menu items and modifiers. The server snapshots the accepted item, modifiers and price into order lines. Staff accept and prepare the order, settle it at the configured boundary, update inventory/prep records, and post loyalty changes where applicable.
GraphQL boundary [#graphql-boundary]
Customer reads use `getCoffeeStore`, `getCoffeeMenu`, `getCafeOrder`, and `getCafeLoyaltyAccount` instead of opening private inventory, recipe, cost, shift, or payment lists. Named commands create pickup orders and transition cafe/POS/catering/order-item/subscription state; apply loyalty; initiate/refund/process payment webhooks; manage menu, lots/inventory, purchase orders, prep, waste, register shifts, staff shifts, and outbox claims/completion.
`createCafePickupOrder` snapshots item/modifier/price facts, applies idempotency, and locks pickup capacity/inventory in its transaction. External clients should use those commands rather than generated writes, while release tests still need to prove store ownership, terminal states, replay, and concurrency.
Setup and onboarding [#setup-and-onboarding]
Use a disposable PostgreSQL database. `runCoffeeOnboarding` creates synthetic store, menu, recipe, inventory, loyalty, supplier, and staff data for local evaluation; repo scripts also exercise that local graph. The seed is not a hosted demo. Run onboarding against an isolated current database and verify its idempotency plus inventory-lot ownership, repair behavior, menu publication, and pickup ordering before evaluating the order path.
Integrations [#integrations]
The registry contains manual, Stripe, and PayPal adapters; webhook ingress is `/api/payment-providers/[providerCode]/webhook`. Online providers are disabled in the synthetic seed. Enable one only after testing environment-only credentials, server-derived amount/currency, provider idempotency, signed raw-body callbacks, replay, terminal states, refunds, and reconciliation. Email, delivery, or third-party POS integrations are not implied by order/provider models.
Security and deployment [#security-and-deployment]
Protect customer, loyalty, payment, cost and provider data. Order creation and inventory deductions need idempotent transactions and concurrent-stock tests. Railway builds then migrates at start; `.env.example` lists PostgreSQL, session, S3, SMTP, provider, and AI variables but does not prove those services.
Deploy only after reviewed migrations, current schema/type/tests/build, cross-store and private-list negatives, pickup capacity, lot consumption, register/staff shifts, snapshot, webhook, refund, restore, and responsive workflow verification. Current source does not establish multi-cafe tenancy, delivery dispatch, third-party POS, enabled online settlement, notification delivery, food-safety compliance, or operating certification.
# External dashboards
To create a custom Construction dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Construction's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Construction codebase before proposing work.
# External storefronts
To create a custom Construction storefront, copy the skill and give it to your LLM. The skill will first explain that current Construction source has no canonical `features/storefront` layer, then inspect the actual public or portal routes and identify any backend contracts needed before treating them as a storefront.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Construction codebase before proposing work.
# Openfront Construction
[Source](https://github.com/openshiporg/openfront-construction) · [Catalog](https://openship.org/products/openfront-construction)
Openfront Construction is an open-source operations backend for general and specialty contractors. It connects project tenancy, preconstruction, cost control, contracts, field execution, safety, progress billing, and controlled agent actions in one relational graph.
Current source provides the domain graph, project portfolio and project workspaces, controlled GraphQL operations, checked migrations, and a synthetic project seed. It does not provide a subcontractor or client portal, interactive company onboarding, file pipeline, accounting connector, payment adapter, or notification worker.
Install and configure [#install-and-configure]
Use Node.js 20 or later and PostgreSQL. From the repository root:
```bash
npm install
```
Create `.env` for the database you intend to use:
```bash
DATABASE_URL=postgresql://user:password@127.0.0.1:5432/openfront_construction
SESSION_SECRET=replace-with-at-least-32-random-characters
PUBLIC_SIGNUPS_ALLOWED=false
```
The development and build scripts deploy checked-in migrations. Review the target and migration SQL before running either command:
```bash
npm run migrate
npm run dev
```
The application is served at `/`, the generated operator dashboard at `/dashboard`, and GraphQL at `/api/graphql`. Do not use `prisma db push` or a destructive reset. Generate migrations only against an approved disposable development database.
Schema, authorization, and source checks that do not deploy migrations include:
```bash
npx keystone build --no-ui
npm run typecheck
npm test
npm run lint
npm run lint:schema
```
Architecture [#architecture]
`Company` is the tenant root and `Project` is the daily operating and security boundary. Company memberships establish tenant access; project memberships narrow operational access. Project-owned records also carry a derived company key so filters and relationship checks do not depend on long mutable relationship chains.
Core links are relational; the generated Prisma schema does not hide domain relationships in JSON. Money uses signed 64-bit integer minor units, percentages and rates use basis points, and estimate versions, document and drawing revisions, contract lines, and progress-claim lines preserve issued facts.
Thirteen high-risk aggregate lifecycles use named GraphQL mutations. Raw status updates are denied for those records. Agent identities, scope grants, action requests, reviews, outcomes, and idempotency keys keep machine work explicit and attributable.
Data model [#data-model]
* **Identity and tenancy:** companies, users, roles, company and project memberships, partners, locations, projects, cost codes, and budget codes.
* **Preconstruction:** bid packages, invitations, submissions and lines, estimates, immutable versions, and estimate lines.
* **Cost control:** budgets, budget lines and changes, direct costs, cost codes, and budget codes.
* **Contracts and change:** prime contracts, commitments, schedule-of-values lines, change events, quotes, change orders, and order lines.
* **Execution:** RFIs and responses, submittals and reviews, specifications, drawings and revisions, documents and versions, daily logs, schedules, dependencies, and punch items.
* **Field and safety:** labor, equipment, material and resource logs, incidents, observations, inspection templates and items, inspections, and corrective actions.
* **Billing:** progress claims and lines, payments and allocations, retainage releases, and lien-waiver evidence.
* **Agents and evidence:** agent identities, scope grants, action requests and outcomes, idempotency keys, and audit records.
Every tenant aggregate has a direct ownership path. Relationship validation rejects company or project mismatches before persistence, and sensitive fields restrict incident details, credentials, waiver evidence, and audit digests.
Workflows [#workflows]
A representative commercial path is bid and estimate -> budget and prime contract or commitment -> field execution -> change exposure and approval -> progress claim -> payment allocation, retainage, and waiver.
Named lifecycle operations cover projects, bid packages, estimates, budget changes, prime contracts, commitments, change events, change orders, RFIs, submittals, progress claims, safety incidents, and inspections. Each operation checks the user permission or machine scope, company and project, allowed transition, reason, idempotency key, and audit attribution.
Agent work follows request -> review -> outcome. A request binds one registered operation, target type and ID, destination status, company, optional project, active agent identity, and an unexpired matching scope grant. Execution revalidates those facts so an approval cannot survive identity or grant revocation.
The platform includes a project portfolio and project workspaces for preconstruction, execution, financials, contracts, field work, documents, quality/safety, billing, team, audit, and reports. Those screens call project-scoped projections and command operations; generic list administration remains available for permitted records.
There is no interactive construction onboarding flow. `npm run seed:demo` uses `DEMO_ADMIN_PASSWORD` and optional `DEMO_ADMIN_EMAIL` to create or reuse a synthetic company, operator membership, project, trade partners, budget/cost records, and sample work through privileged context. The script requires a ten-character password but does not restrict `DATABASE_URL` to a disposable or loopback host, so inspect the selected database before running it. Seeded project data is not a hosted demo or customer-safe default.
Bounded GraphQL operations [#bounded-graphql-operations]
Current project queries return a portfolio and one company/project-scoped workspace rather than exposing every construction list to the UI. Named mutations create projects and team assignments; start bid packages and daily logs; record direct costs, change events, RFIs, submittals, documents, punch items, safety observations, and agent requests/reviews/outcomes; and transition protected project, bid, estimate, budget, contract, change, execution, safety, billing, lien-waiver, and payment lifecycles. Each operation rechecks tenant/project scope, permission, expected state, reason, idempotency, and evidence as applicable.
Integrations [#integrations]
Document object storage and scanning, signatures, accounting and ERP sync, payments, notifications, and background delivery remain external adapter work. Existing document, payment, and audit records define domain ownership and evidence; they do not establish a provider connection.
An accounting adapter should map explicit company/project, cost-code, contract, change, claim, payment, and allocation records; use idempotent external links; preserve integer money and currency; and reconcile rather than overwrite local history. File and signature adapters should preserve version checksums, actor and provider evidence, callbacks, and retention policy. Payment and notification adapters need signed ingress, replay protection, retry state, and an outbox or worker.
Security [#security]
List access applies company and project filters, and relationship hooks reject cross-tenant links. Sensitive lifecycle mutations recheck ownership before privileged database access. Machine grants are least-privilege, exact-company and optional exact-project, bounded by operation scope and expiry, and revalidated at execution.
Completion/MCP transport uses a request-local fetch wrapper, forwards cookies only to the exact same-app endpoint, rejects redirects and caller cookie overrides, and leaves process-global fetch unchanged. GraphQL enforces a depth limit and restricts unsafe GET behavior. These controls do not replace ingress rate and body limits, managed secrets, centralized logs, incident response, backup restoration, provider security, or cross-company penetration testing.
Before real contracts, safety, lien-waiver, or payment data, verify role and membership grants, company/project negatives, lifecycle denial, concurrent financial allocations, agent revocation, file and provider callbacks, retention, backups, and the legal, labor, safety, payment, and privacy requirements for the deployment.
Deployment [#deployment]
Current source contains nine checked-in migration directories: seven inherited starter migrations followed by the construction schema and platform-operation migrations. Both `npm run dev` and `npm run build` currently execute `npm run migrate`; building with an uncontrolled `DATABASE_URL` can therefore change that database. Use isolated build credentials or split migration from application build before adopting immutable-image or independently promoted deployments.
For a deliberate release, back up and verify the target, review every migration, deploy it once, build against an approved environment, run schema/security tests and authenticated company/project negatives, exercise each protected lifecycle and agent-revocation path, then test rollback and restoration. Current source does not supply file storage/scanning, accounting, signatures, payment processing, notification delivery, production observability, or a complete external portal. Add and test those services before using the application as an operational system.
Extension paths [#extension-paths]
* Add lists in the appropriate bounded-context file under `features/keystone/models` and register them explicitly. Keep direct company/project relationships and use relational fields rather than hiding core links in JSON.
* Add aggregate transitions to `features/keystone/domain/lifecycle.ts` and `features/keystone/mutations/lifecycle.ts`; preserve raw status denial, adjacency tests, idempotency, and immutable audit evidence.
* Add agent capabilities in `features/keystone/domain/scopes.ts` and the controlled agent mutation path. Bind them to exact target types and tenant/project scope and revalidate grants at execution.
* Add file, accounting, signature, payment, and notification adapters behind narrow domain operations. Store provider references and redacted evidence, not credentials or unbounded payloads, and test duplicate callbacks and reconciliation.
* Build project, field, safety, and billing surfaces under `app` or `features/dashboard`; call domain operations rather than reimplementing transition and permission rules in the browser.
* Extend `tests/schema` and `tests/security` with tenant negatives, relationship mismatches, lifecycle rejection, money/allocation invariants, concurrency, agent revocation, and provider failure cases before enabling new work.
# External dashboards
To create a custom Convenience dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Convenience's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Convenience codebase before proposing work.
# External storefronts
To create a custom Convenience storefront, copy the skill and give it to your LLM. The skill will first explain that current Convenience source has no canonical `features/storefront` layer, then inspect the actual public or portal routes and identify any backend contracts needed before treating them as a storefront.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Convenience codebase before proposing work.
# Openfront Convenience
[Source](https://github.com/openshiporg/openfront-convenience) · [Catalog](https://openship.org/products/openfront-convenience)
Openfront Convenience is an open-source operations application for convenience stores. It connects scan-and-price POS, tenders and receipts, cash control, purchasing and receiving, lot-aware inventory, loyalty, regulated-sale checks, prepared food, and optional fuel records in one business-owned PostgreSQL system.
Payment capture, signed payment webhooks, and fuel-controller protocols require explicit provider adapters. Current source can record configured external references and outcomes, but it does not claim an external effect that an adapter did not execute and reconcile.
Install and configure [#install-and-configure]
Use Node.js 20 or later and PostgreSQL. From the repository root:
```bash
npm install
```
Create `.env` for the intended local database:
```bash
DATABASE_URL=postgresql://127.0.0.1:5432/openfront_convenience
SHADOW_DATABASE_URL=postgresql://127.0.0.1:5432/openfront_convenience_shadow
SESSION_SECRET=replace-with-at-least-32-random-characters
INTERNAL_APP_ORIGIN=http://127.0.0.1:3000
PUBLIC_SIGNUPS_ALLOWED=false
```
`INTERNAL_APP_ORIGIN` is the trusted destination for cookie-bearing internal requests. In a deployed environment it must be the application's fixed origin; request host headers do not select it.
Apply checked-in migrations and start development:
```bash
npm run migrate
npm run dev
```
`npm run dev` also deploys migrations before starting Next.js, so point `DATABASE_URL` only at the database you intend to change. Do not use schema push or reset commands. The app is served at `/`, the operator dashboard at `/dashboard`, and GraphQL at `/api/graphql`.
Architecture [#architecture]
`Business` is the tenant root. Active employee memberships and business-owned roles determine permissions. Stores, employees, registers, inventory, pricing, purchasing, sales, integrations, and audit records carry a direct business relationship. Consequential operations also validate store ownership before using serializable Prisma transactions.
Money uses integer minor units, rates use basis points, and stock and weighted quantities use fixed four-decimal arithmetic. Inventory, cash, loyalty, idempotency, and audit history is append-only through the public API. Completed sale lines retain product, price, tax, promotion, and compliance snapshots.
Fuel is an optional bounded module. Retail catalog, sales, purchasing, and inventory do not depend on a fuel-controller connection.
Data model [#data-model]
* **Business and stores:** businesses, roles, employees, store assignments, stores, registers, shifts, sessions, store days, and cash movements.
* **Catalog and pricing:** products, variants, categories, barcodes, tax categories and rates, price books and entries, promotions, rules, and redemptions.
* **Inventory and purchasing:** locations, stock levels, lots, ledger entries, counts and lines, vendors and items, purchase orders and lines, receiving and lines, and waste.
* **Sales:** carts and lines, sales and immutable sale lines, tenders, refunds and lines, returns and lines, and original-lot restocking links.
* **Customers and compliance:** loyalty programs, accounts and ledger entries, compliance policies, age-verification evidence, and operational alerts.
* **Prepared food:** recipes, ingredients, prep batches, output lots, ingredient consumption, and waste.
* **Fuel:** sites, grades, tanks and readings, pumps and nozzles, effective prices, deliveries, transactions, and reconciliation.
* **Operations:** payment-provider records, API keys, webhook events, scoped idempotency keys, and audit events.
Provider and controller configuration accepts opaque secret references such as `env://...` or `vault://...`; inline secret-like values are rejected.
Workflows [#workflows]
The primary POS path is barcode scan -> server-side price, promotion, tax, and compliance validation -> sale snapshot and tenders -> receipt -> inventory and cash ledger postings.
Named GraphQL operations also cover:
* first-business onboarding with owner role, store, register, inventory locations, price book, tax defaults, and active membership;
* shift and register-session opening and closing, balanced cash movement, expected cash, and over/short reconciliation;
* cash sales, split tenders, partial/full refunds, returns, and restoration to original inventory lots;
* purchase-order submission and approval, partial receiving, rejected quantities, lot creation, and stock posting;
* inventory adjustments, counts, count approval, FIFO lot consumption, and idempotent replay;
* promotion application, loyalty-ledger adjustments, and minimum-data age verification;
* recipe consumption, prep output lots, and waste posting;
* fuel delivery reconciliation and completed dispense recording; and
* one-time API-key issuance and revocation.
Purpose-built platform routes cover overview, POS, inventory, pricing, purchasing, restricted sales, loyalty, prepared food, fuel, reconciliation, exceptions, reports, and operations. `/dashboard/onboarding` handles first-business setup, while generic administration remains available for permitted records.
Bounded GraphQL operations [#bounded-graphql-operations]
The platform reads business-scoped operational and reporting projections. Named mutations onboard a business; open/close shifts and register sessions; create and price carts; complete and refund sales; receive purchase orders; post inventory adjustments and counts; apply promotions; adjust loyalty; verify restricted-sale age; run prepared-food and waste work; record fuel prices, deliveries, tank readings, transactions, and reconciliation; manage store-day/bank-deposit reconciliation; and issue/revoke API keys. Outbox claim, completion, replay, and failure state are separate worker operations. Generated CRUD is not a substitute for these transaction, fixed-point, lifecycle, and evidence boundaries.
Integrations [#integrations]
Checkout and refund operations can record cash and externally confirmed non-cash references. They do not call an unconfigured payment provider. A payment adapter needs server-owned amount and currency, provider idempotency, signed raw-body webhook verification, event replay protection, controlled state transitions, and reconciliation.
Fuel models record controller and device references without coupling the domain to one protocol. A controller adapter needs exact store/site scope, secret references, device mapping, signed or mutually authenticated ingress, duplicate-event handling, sequence and meter reconciliation, offline recovery, and an operator-visible exception path.
The same adapter rule applies to accounting, loyalty, tax, ordering, and notification providers: a configuration row is not proof of execution.
Security [#security]
Tenant filters hide other businesses, and relationship access rejects cross-business connections. Consequential mutations recheck business and store scope. Sale, inventory, cash, loyalty, idempotency, and audit evidence cannot be rewritten through ordinary GraphQL CRUD.
Employee PINs and API tokens are one-way hashed. API-key plaintext is returned once and excluded from idempotency and audit snapshots. Restricted-sale evidence stores a cryptographic fingerprint and decision metadata rather than raw identity numbers, full birth dates, or document scans. Completion transport uses request-local cookie forwarding to the exact configured internal origin and does not patch process-global fetch.
Before real operations, verify role grants, cross-business negatives, register and inventory concurrency, refund authorization, provider callback handling, secret management, backups, monitoring, rate limits, physical register controls, and applicable payment, tax, age-restriction, food-safety, fuel, and privacy requirements.
Deployment [#deployment]
Current source contains eleven checked-in migration directories: seven inherited starter migrations and four Convenience migrations covering the operating graph, return-lot allocation, store-day/fuel reconciliation, and durable operation/outbox records. `npm run build` generates Keystone artifacts and builds Next.js without deploying migrations; `npm run dev` deploys them first. No hosted Convenience demo is advertised by these docs.
For deployment, review and apply migrations separately, use stable secrets and a fixed `INTERNAL_APP_ORIGIN`, back up and test restoration, and run current schema tests, completion-transport tests, typecheck, build, tenant negatives, register/shift control, sale/refund, purchasing/receiving, FIFO lots, prepared-food, fuel records, API-key, and outbox replay flows. Configure only implemented adapters and stage rollout with monitoring and rollback. Those local paths do not establish real payment, fuel, accounting, tax, loyalty, or notification execution, and the application must not be deployed with inherited or synthetic credentials.
Extension paths [#extension-paths]
* Add domain records under `features/keystone/models` and register them in `features/keystone/models/index.ts`; preserve direct business ownership and generate a reviewed migration.
* Add consequential behavior under `features/keystone/mutations` using the existing operation runner, fixed-point helpers, tenant checks, idempotency, serializable transactions, and append-only evidence.
* Extend first-business defaults in `features/platform/onboarding` without making seed data the authority for runtime permissions or prices.
* Add payment, fuel, accounting, tax, loyalty, or notification adapters behind the corresponding provider boundary. Keep secrets out of model JSON and prove callback authentication, replay, failure, and reconciliation behavior.
* Build cashier and operator surfaces under `app` or `features/dashboard`; call named domain operations instead of reproducing POS, refund, receiving, or inventory rules in the browser.
* Add tests for cross-business access, fixed-point arithmetic, lot allocation, oversell/concurrency, idempotent replay, lifecycle denial, callback duplication, and failed external effects before enabling a new workflow.
# External dashboards
To create a custom Dealership dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Dealership's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Dealership codebase before proposing work.
# External storefronts
To create a custom Dealership storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Dealership's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Dealership codebase before proposing work.
# Openfront Dealership
[Source](https://github.com/openshiporg/openfront-dealership) · [Catalog](https://openship.org/products/openfront-dealership)
Openfront Dealership combines a public vehicle inventory with the operator work that follows an inquiry: leads, test drives, trade-ins, deals and approvals, finance intake, service appointments, repair orders, parts, tasks, and reporting.
Finance forms are lead intake, not underwriting or lender decisions. Current source does not establish payment processing, credit compliance, identity verification, e-signature, or DMS/accounting parity.
Architecture and schema [#architecture-and-schema]
`DealerGroup` and active group membership are the tenant root; `Rooftop` and RooftopMembership narrow staff and operational scope. Relationship hooks and custom commands validate rooftop ownership for submitted IDs. The graph includes dealer groups, rooftops and memberships; makes/models, vehicles, images, history, condition and price records; inventory acquisition, stock, feed, merchandising and reconciliation records; leads and interactions; test drives and trade-ins; deals and approvals; financing applications and lender submissions/decisions; service appointments, service orders, repair lines/packages and parts; delivery work, tasks, audit/outbox events, and dealership profiles.
Public routes cover inventory/detail/compare, contact, financing, service, test-drive, and trade-in intake. Operator routes under `/dashboard/platform` cover overview, vehicles, CRM, deals, delivery, finance, test drives, trade-ins, service, and analytics.
Main workflow [#main-workflow]
A buyer browses a published vehicle and submits an inquiry, test-drive request, trade-in, finance inquiry, or service request. Staff qualify the lead, schedule work, and move a deal or service order through controlled internal steps. Sensitive financial inputs should be minimized and routed through a purpose-built secure boundary rather than copied into generic notes.
Bounded GraphQL operations [#bounded-graphql-operations]
Public intake mutations submit inquiries, test drives, trade-ins, financing, and service requests with rooftop scope and abuse controls. `operatorProjections`, `operatorWorkspaces`, and `inventoryWorkspace` provide task reads. Named commands cover lead ownership/activity, deal pencils and transitions, financing queue/decisions, inventory acquisition/stock/feed/merchandising/reconciliation, test-drive workflow, trade-in appraisal, service orders, tasks, membership changes, and delivery completion. Finance operations record applicant and lender workflow evidence; they do not underwrite or originate credit.
Setup and onboarding [#setup-and-onboarding]
Use a disposable PostgreSQL database. `runDealershipOnboarding` creates a synthetic dealer group, rooftop, membership, profile, and evaluation records. Run it against an isolated current database, verify that a second execution does not duplicate tenant identity, and inspect all seeded names, inventory, leads, applications, and appointments before publishing screenshots. The local seed is not a hosted demo.
Integrations [#integrations]
The current payment decision is N/A because deal and service fields record externally handled facts. There is no payment adapter directory. Inventory-feed and financing/lender models/routes are local intake and evidence boundaries; no lender, credit bureau, DMS, OEM, e-signature, accounting, identity, or payment adapter was found. Do not claim external execution until adapter authentication, consent, mapping, retries, reconciliation, and privacy controls are implemented and tested.
Security and deployment [#security-and-deployment]
Protect income, credit-range, contact, vehicle-history, deal, approval, and service records. Deal and approval transitions need immutable evidence and restricted raw edits. Railway builds then migrates at start. Current source has targeted onboarding/payment/platform checks, but the repository README's `env.example` instruction points to a missing file and must not be copied as a setup guarantee.
Before deployment, verify public-intake rate/abuse controls, cross-group/rooftop and membership denial, relationship changes, financing-field privacy, inventory acquisition/feed reconciliation, deal/service terminal states, reviewed migrations, audit/outbox, restore, and responsive customer/operator routes. Current source does not underwrite credit, originate loans, settle payments, provide external vehicle history, or establish dealer/lender/compliance certification.
# AI assistant
Openfront includes an AI assistant inside the admin dashboard. It is not a generic help bot. It connects to the same GraphQL API the rest of the product uses, inspects the schema through MCP tools, and then runs real queries or mutations based on what you ask.
That means the assistant is useful for actual store work, not just answering documentation questions.
What it is good at [#what-it-is-good-at]
* looking up products, orders, customers, and other records
* creating or updating data without you having to remember exact mutation names
* making repetitive admin tasks faster
* helping operators move around a large schema with plain language instead of GraphQL syntax
How it works [#how-it-works]
It inspects the live schema [#it-inspects-the-live-schema]
The assistant talks to the MCP transport exposed by the app and discovers models, fields, and operations before it makes a change.
It maps your request to the right model [#it-maps-your-request-to-the-right-model]
If you ask for something like "find the Blue Hoodie" or "update this region's currency settings," the assistant resolves that request to the correct GraphQL types and operations.
It executes the same API the app already uses [#it-executes-the-same-api-the-app-already-uses]
The actual work still happens through GraphQL. There is no hidden admin backdoor.
It respects your session [#it-respects-your-session]
The assistant runs with your current permissions. If your account cannot edit products or view certain records, the assistant cannot do that either.
Useful prompts to start with [#useful-prompts-to-start-with]
Try prompts like these inside the dashboard:
* "Find products with Penrose in the name."
* "Increase the price of the Summer Collection by 10 percent."
* "Show me unpaid orders from this week."
* "Create a discount code for 15 percent off."
* "List payment providers available in Europe."
Where it lives in the codebase [#where-it-lives-in-the-codebase]
If you want to customize the assistant, start with these files:
* `app/api/completion/route.ts`
* `app/api/mcp-transport/[transport]/route.ts`
* `features/dashboard/hooks/use-chat-submission.tsx`
* `features/dashboard/components/dual-sidebar/ai-chat-sidebar.tsx`
Configuration [#configuration]
The dashboard supports two main ways to run the assistant.
Shared keys [#shared-keys]
Use platform-managed OpenRouter credentials when you want the team to share a central setup.
Local keys [#local-keys]
Use your own OpenRouter key when you want per-user control over model choice and cost.
The current dashboard exposes those options through the assistant settings UI.
Supported models [#supported-models]
For data-heavy admin work, higher-reasoning models tend to hold up better. A practical starting point is:
* `anthropic/claude-3.5-sonnet`
* `openai/gpt-4o`
What to be careful about [#what-to-be-careful-about]
* The assistant is only as safe as your access rules.
* You should still review bulk changes before trusting them.
* Natural language is convenient, but it is not a substitute for good permissions and validation.
Think of the AI assistant as an admin operator that already knows your schema. It is not a customer-facing chat widget, and it should not be treated like one.
# GraphQL API
Openfront's Keystone layer exposes GraphQL at `/api/graphql`. The schema is generated from `features/keystone/models` and the registered custom mutations in `features/keystone/mutations`.
There is no static hosted API reference that can stay accurate for every Openfront fork. Inspect `schema.graphql` from your source revision or use introspection on an authorized non-production environment. Do not send production credentials to a public demo playground.
Session and bearer authentication [#session-and-bearer-authentication]
Dashboard requests use the application session. Current source also contains API-key and OAuth bearer-token handling. API keys have hashed secret material, status/expiry fields, optional IP restrictions and scope data.
Before relying on bearer scopes, test that each resolver and custom operation enforces the expected scope and tenant/ownership boundary. A scope value stored on a key does not protect an operation unless the operation checks it.
Query published products [#query-published-products]
The generated schema uses `title`, `handle`, `status`, `productVariants`, `productImages`, and `productCategories`:
```graphql
query PublishedProducts {
products(where: { status: { equals: published } }) {
id
title
handle
thumbnail
status
productVariants {
id
title
sku
}
productImages {
id
image {
url
}
imagePath
altText
}
productCategories {
id
title
handle
}
}
}
```
Run queries against your own origin:
```bash
curl https://your-openfront.example/api/graphql \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_KEY' \
--data '{"query":"query { products(take: 1) { id title handle } }"}'
```
Use a key with only the scope needed for the operation. Keep it out of shell history and logs.
Custom operations [#custom-operations]
Checkout, carts, payments, fulfillment, OAuth, apps and other high-value workflows use custom GraphQL operations in addition to generated list CRUD. Read the registered source and generated schema for exact names and inputs. Prefer those domain operations when they enforce ownership, snapshots, idempotency and lifecycle checks.
Generated CRUD is not automatically a safe public storefront API. Verify anonymous, wrong-user, wrong-store, expired-key, missing-scope and replay cases before exposing any operation.
Schema visualizer [#schema-visualizer]
The [schema visualizer](/docs/openfront/ecommerce/schema-visualizer) can help with relationships. The generated `schema.graphql` remains the authoritative API artifact for a source revision.
# B2B & Credit Accounts
Openfront includes B2B account flows for stores that need more than normal retail checkout. That usually means business account requests, approved credit limits, payment terms, and API-backed ordering for larger customers.
Business account workflow [#business-account-workflow]
Request submission [#request-submission]
A signed-in user can submit a business account request with details like:
* business type
* expected monthly volume
* requested credit limit
* a short description of the business
Admin review [#admin-review]
Store staff review the request in the dashboard and decide whether to approve it.
That review can include:
* internal notes
* adjusted credit limits
* approval or rejection based on the store's process
Account setup after approval [#account-setup-after-approval]
When approved, Openfront can create the linked account records, apply the credit terms, and generate the token or access needed for API ordering.
Credit and billing behavior [#credit-and-billing-behavior]
For approved B2B accounts, the main difference is that the order flow does not have to behave like ordinary pay-now retail checkout.
That can include:
* credit limit tracking
* used versus available credit
* billing cycles
* outstanding balances
* account-level payment terms
API ordering for partners [#api-ordering-for-partners]
B2B partners can use their customer token to place orders programmatically.
Typical use cases:
* automated inventory checks
* headless reordering
* partner portals
* bulk ordering from an external system
Where this is useful [#where-this-is-useful]
* wholesale accounts
* reseller programs
* distributors
* business buyers with invoicing or payment terms
The important distinction is that these flows are account-driven, not just discount-code-driven. The platform treats them as a different operating model, which is what B2B stores usually need.
# Comparisons
Openfront is an open-source commerce application that combines a Next.js storefront and operator dashboard with a Keystone GraphQL backend and PostgreSQL. Its main difference from a hosted commerce service is operational ownership: you run the application, database, migrations, credentials, and integrations.
Compare operating models [#compare-operating-models]
| Approach | What you operate | Customization boundary | Typical tradeoff |
| ------------------------- | -------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------- |
| Openfront | Application source, database, deployment, and provider configuration | The full application and backend source | More control, with responsibility for upgrades, security, reliability, and operations |
| Hosted commerce service | Store configuration and extensions allowed by the service | Themes, apps, webhooks, and published APIs | Less infrastructure work, but behavior and data access remain bounded by the service |
| Plugin-based commerce | A CMS, commerce plugin, database, and plugin set | Themes, plugins, and custom server code | Broad extension ecosystem, with plugin compatibility and maintenance work |
| Enterprise commerce suite | Vendor platform plus an implementation and integration layer | Vendor extension points and contracted APIs | Broader packaged programs, with higher implementation and operational complexity |
| Custom build | Every selected application and backend component | Whatever the team designs | Maximum design freedom, with no prebuilt Openfront domain model or workflow baseline |
These categories are not feature-for-feature equivalents. Compare the exact source revision, provider contracts, hosting model, support arrangement, and workflows required by your operation.
What current Openfront source includes [#what-current-openfront-source-includes]
The Ecommerce source currently contains:
* a regional storefront with catalog, cart, checkout, confirmation, and customer-account routes;
* a custom operator dashboard for catalog, orders, inventory, fulfillment, markets, payments, shipping, discounts, users, API keys, and settings;
* Keystone models and GraphQL operations backed by PostgreSQL and Prisma migrations;
* built-in Stripe, PayPal, and manual payment paths;
* built-in shipping modules plus database-configured local or HTTP adapter operation fields;
* one Store configuration consumed by the current dashboard and storefront.
This inventory describes source code; it is not evidence that every workflow has completed the testing required by a deployment.
Where Openfront offers control [#where-openfront-offers-control]
Because the application source is available to the operator, a team can change its storefront, dashboard, models, GraphQL operations, and integration adapters. The operator also chooses the hosting and database environment.
That control does not make integration work automatic or unconstrained. A new provider still requires a compatible operation contract, secure credential handling, lifecycle integration, failure recovery, and tests. Current payment completion also contains provider-specific branches, and the custom shipping-provider forms do not fully populate external operation URLs.
Costs and transaction fees [#costs-and-transaction-fees]
Openfront does not establish the fees charged by payment providers, shipping services, infrastructure vendors, marketplaces, or implementation partners. Self-hosting also carries database, compute, storage, monitoring, maintenance, and engineering costs. Evaluate those costs against the fees and operating work of an alternative platform rather than assuming that access to the source removes operating costs.
Security, privacy, and compliance [#security-privacy-and-compliance]
Open source and self-hosting do not establish security, privacy, tax, accessibility, or regulatory compliance. Those outcomes depend on the deployed source revision, configuration, hosting, data flows, contracts, operational controls, and independent review.
Current source contains access-control, session, API-key, OAuth, and provider-integration mechanisms, but each deployment must test ownership and tenant boundaries, secret storage, outbound HTTP policy, webhook verification, retries, reconciliation, backups, and incident response.
When Openfront may fit [#when-openfront-may-fit]
Consider Openfront when a team:
* wants to own and modify the commerce application source;
* can operate a Next.js, Keystone, PostgreSQL, and Prisma stack;
* needs workflows that justify source-level customization;
* is prepared to test and maintain its payment, shipping, tax, identity, and deployment boundaries.
A hosted or packaged alternative may fit better when rapid vendor-managed setup, an existing certified connector, contracted support, or an established compliance program matters more than source-level control.
Migration expectations [#migration-expectations]
Migration is an implementation project, not a built-in one-click feature. Inventory source-platform exports, map identities and relationships, define money and status semantics, migrate media and secrets separately, reconcile counts and totals, and rehearse rollback before cutover. Build or verify the import tooling required for the exact source and target systems.
# Deployment
Openfront Ecommerce needs a Node.js runtime and persistent PostgreSQL. S3-compatible storage, email, payment, shipping, AI and marketplace secrets depend on the features you enable.
The repository's `npm run build` currently generates the Keystone schema, deploys Prisma migrations, and runs the Next.js build. Do not let multiple build workers race to migrate production. Split migration ownership into one reviewed release job where your host permits it.
Required planning [#required-planning]
1. Pin and review the exact source revision.
2. Generate the Keystone/Prisma schema and run static checks.
3. Back up production and test every pending migration on a realistic disposable clone.
4. Supply `DATABASE_URL` and a random `SESSION_SECRET` through the host's secret manager.
5. Supply only the provider/storage/email variables used by the deployment.
6. Apply migrations once, deploy, then run anonymous, authenticated and wrong-owner checks.
7. Verify checkout totals, inventory, provider signatures/replay, fulfillment, refund and failure recovery.
8. Keep an application rollback and database forward-recovery plan.
Hosting buttons [#hosting-buttons]
The source repository and catalog may provide Railway, Vercel or other convenience templates. They can create infrastructure, but they cannot verify your migration history, database sizing, object storage, background work, provider webhooks, security headers, backups or compliance needs.
Openfront can run on a Node-capable host with PostgreSQL if those requirements are met. “Deploy succeeded” is not the same as “commerce workflow is safe.”
Before putting customer or payment data into the instance, verify authorization, credentials, webhooks, concurrency, migrations, backups, restoration, and rollback against the exact release.
# Getting started
This guide follows the current repository scripts and `.env.example`. It sets up a local evaluation, not a production store.
Prerequisites [#prerequisites]
* Node.js 20 or newer
* PostgreSQL
* Git and npm
* an empty, isolated database
Clone and install [#clone-and-install]
```bash
git clone https://github.com/openshiporg/openfront.git
cd openfront
npm install
```
Create `.env` [#create-env]
Copy the checked-in example and replace its placeholders:
```bash
cp .env.example .env
```
The required core values are:
```bash
DATABASE_URL="postgresql://username:password@localhost:5432/openfront"
SESSION_SECRET="replace-with-a-random-string-at-least-32-characters"
```
The example also lists SMTP, S3-compatible storage, Stripe, PayPal, OpenRouter, default-region, branding and marketplace-token variables. Configure only the integrations you will test, and keep every real secret out of source control.
Review migrations and start development [#review-migrations-and-start-development]
```bash
npm run dev
```
The current script runs Keystone schema generation, `prisma migrate deploy`, and the Next.js development server. Review the target `DATABASE_URL` and migration history before running it.
Create the initial dashboard user [#create-the-initial-dashboard-user]
Open `/dashboard/init` on the local origin if the database has no user, then sign in at `/dashboard`.
Apply the demo setup [#apply-the-demo-setup]
Use the dashboard onboarding flow or the repository's guarded seed command for its intended local runtime. The checked-in seed describes the Impossible Tees store, three regions, products, variants, regional prices, categories, collections and provider records.
Run onboarding twice only on an isolated evaluation database and confirm it reuses stable business keys.
What to check [#what-to-check]
* `/` redirects or resolves to the configured default country/region storefront.
* `/dashboard` requires the expected session and shows the configured store.
* `/api/graphql` reflects the generated schema from this exact source.
* published products have variants and prices for the region you are browsing.
* cart and order operations reject guessed or mismatched ownership.
* disabled or unconfigured providers fail closed.
Do not use a root HTTP 200 or seeded products as proof that checkout, provider webhooks, refunds, shipping, API-key scopes or OAuth are safe. Test the exact workflow and negative cases you intend to deploy.
Next [#next]
* [Storefront](/docs/openfront/ecommerce/storefront)
* [Payment providers](/docs/openfront/ecommerce/payment-providers)
* [Deployment](/docs/openfront/ecommerce/deployment)
# Gift Cards
Gift cards in Openfront are treated as a real balance, not just a one-time coupon code.
That means a gift card can be created, issued to a customer, redeemed across more than one order, and tracked through its transaction history.
What the gift card flow covers [#what-the-gift-card-flow-covers]
* custom gift card amounts
* unique gift card codes
* balance tracking
* expiration dates when used
* transaction records for redemption activity
Typical lifecycle [#typical-lifecycle]
1. a gift card is created in the dashboard or sold as a product
2. the recipient gets the code
3. the code is applied during checkout
4. Openfront checks whether the card is valid and how much balance remains
5. the order total is reduced and the transaction is recorded
Why it matters [#why-it-matters]
Gift cards are easy to describe and surprisingly annoying to implement well. The useful part is not just the code itself. It is the accounting around partial use, remaining balance, and transaction history.
That is the part Openfront already models.
# Openfront Ecommerce
[Source](https://github.com/openshiporg/openfront) · [Catalog](https://openship.org/products/openfront-ecommerce)
Openfront Ecommerce is Openfront's commerce product. It combines a country-aware storefront, an operator dashboard, a Keystone GraphQL/data layer, and provider adapters in one source repository that the operator controls.
Before using real customer or payment data, verify cart ownership, adapter consistency, provider webhooks, type/build enforcement, and the complete checkout path in the deployed environment.
What the source covers [#what-the-source-covers]
* catalog: products, variants, options, images, categories, collections, types and tags;
* regional commerce: stores, regions, countries, currencies, price sets/lists/rules and tax records;
* inventory and fulfillment: locations, stock movements, shipping profiles/options/providers, labels and fulfillment lines;
* checkout and orders: carts, addresses, payment sessions/collections, orders, immutable order lines, captures, refunds, returns, swaps and claims;
* business workflows: customer groups, business-account requests, invoices, checkout links, gift cards, discounts and draft orders;
* extensibility: API keys, OAuth apps/tokens, webhooks, payment/shipping adapters and Openship installation records.
Architecture [#architecture]
Ecommerce establishes the family pattern: thin App Router pages, operator work in `features/platform`, storefront work in `features/storefront`, schema and GraphQL operations in `features/keystone`, and provider code in `features/integrations`.
Its model names are specific to commerce. Other verticals reuse the boundaries, not the catalog/order vocabulary.
Main workflow [#main-workflow]
A store publishes products and regional prices. A guest or customer creates a cart, selects shipping and payment, and completes checkout. The server writes order and money snapshots so later catalog changes do not rewrite the sale. Operators fulfill, refund, return, swap or claim against that recorded order.
Setup, onboarding and deployment [#setup-onboarding-and-deployment]
Use [Getting started](/docs/openfront/ecommerce/getting-started) for a source-backed local path. The checked-in onboarding seed creates the Impossible Tees demo across store, regions, products, variants, prices, categories, collections and provider records. Setup data does not prove external provider execution.
Read [Deployment](/docs/openfront/ecommerce/deployment) before choosing a host. Several package commands apply migrations as part of development or build, so release jobs need deliberate migration ownership.
# Marketplace & OAuth
Openfront includes OAuth-based integration flows for stores that want to connect external apps or build an app ecosystem around the platform.
This is useful when another system needs controlled access to store data without sharing a full admin session.
The OAuth flow [#the-oauth-flow]
App registration [#app-registration]
An app is registered with the credentials and redirect URLs it needs.
That usually includes:
* client ID and secret
* redirect URIs
* requested scopes
User authorization [#user-authorization]
When the app wants access, the user is sent to an authorization screen where they can see what the app is asking for.
Installation and token use [#installation-and-token-use]
Once approved, the app receives the token it needs to call the Openfront API within the granted scope.
Why this matters [#why-this-matters]
OAuth is the safer path when you want third-party access without turning every integration into a custom one-off token exchange.
Typical use cases:
* external order-management systems
* marketplace connectors
* custom partner apps
* internal tools that should not run as full dashboard users
Related concepts [#related-concepts]
These integrations often sit alongside:
* scoped app permissions
* idempotency handling for repeated requests
* webhook subscriptions for syncing data changes
If you are building apps around Openfront instead of just running a single storefront, this is one of the features that starts to matter quickly.
# Payment providers
Openfront stores payment-provider configuration in `PaymentProvider` records and dispatches standardized operations through `features/keystone/utils/paymentProviderAdapter.ts`.
A provider can use either:
* a local module token such as `stripe`, `paypal`, or `manual`; or
* an HTTP endpoint for an independently deployed adapter app.
Each operation field is configured separately: creation, capture, refund, status, payment link, and webhook handling. Database-selected HTTP routes are part of the adapter design, allowing merchants to connect external payment apps without adding their implementation to the Openfront repository.
Dispatch contract [#dispatch-contract]
For HTTP fields, Openfront sends `POST` JSON containing the queried provider record and operation-specific arguments. For local fields, it dynamically imports `features/integrations/payment/.ts` and calls the export matching the operation field.
The `features/integrations/payment/index.ts` object lists built-in modules, but the current Keystone dispatcher does not use that object as a static registry. The operation fields themselves control dispatch.
Data boundary [#data-boundary]
Payment providers, sessions, collections, payments, captures, and refunds attach provider-neutral state to carts, invoices, or orders. The adapter may require credentials and payment/customer context to call its upstream service, but Openfront should forward only what that operation needs and validate the adapter response before changing domain state.
Provider configuration is privileged: changing an operation URL changes which trusted app receives provider and payment data. Protect that configuration with operator authorization, endpoint policy, Openfront-to-adapter authentication, secret handling, redaction, timeouts, response schemas, idempotency, and reconciliation.
The current bridge does not provide those hardening controls automatically. It sends the selected provider object to any operation value beginning with `http`, with only a JSON content-type header. Review and harden this boundary for the deployment while preserving external adapter support.
Current workflow boundaries [#current-workflow-boundaries]
Generic adapter dispatch is implemented for payment initiation and webhook handling, and helper functions exist for capture, refund, status, and payment links. The overall Ecommerce workflow is not fully generic:
* provider creation UI currently exposes built-in presets rather than full route-app configuration;
* checkout and invoice completion still contain hard-coded Stripe, PayPal, and manual code switches;
* several adapter helpers are not consistently used by the lifecycle paths;
* raw-body webhook requirements, replay handling, and external route authentication need target-specific verification.
A provider row or successful initiation call therefore does not prove complete payment processing.
Enabling a provider [#enabling-a-provider]
1. Choose local-module or external-route mode for each operation.
2. Configure the provider record and regions through a privileged path.
3. verify exact request and response shapes from the target revision.
4. derive amounts, currencies, actor/cart/order identity, and allowed transitions on the server.
5. add idempotency and unknown-outcome reconciliation.
6. verify webhook origin/signature and replay handling.
7. run initiation, browser handoff, completion, capture, refund, failure, timeout, and recovery tests.
Manual payment [#manual-payment]
Manual is an offline/operator boundary. It must not let an anonymous customer grant paid state or entitlement without the intended authorized settlement step.
Start with [Add a payment adapter](/docs/openfront/ecommerce/how-to-guides/custom-payment-provider).
# Price Lists
Price lists let Openfront override a product's usual price when a different pricing rule should win.
That is useful for things like:
* wholesale pricing
* VIP customer groups
* regional pricing differences
* scheduled sales windows
* currency-specific pricing that is not just a live conversion
How price lists work [#how-price-lists-work]
When Openfront needs to resolve a price, it checks the active pricing context.
That usually includes:
* who the customer is
* whether they belong to a customer group
* which region or currency is active
* whether a scheduled pricing window is live
If a price list applies, it can replace the standard price for that context.
Common use cases [#common-use-cases]
B2B pricing [#b2b-pricing]
A wholesale account may see a lower price than a retail customer.
Regional pricing [#regional-pricing]
The UK market may have its own price points instead of a straight conversion from US pricing.
Scheduled sales [#scheduled-sales]
A seasonal or flash-sale price list can turn on and off automatically.
Why teams use it [#why-teams-use-it]
Price lists are what you reach for when one base price is no longer enough.
Discounts are often better for promotions. Price lists are better when you want the alternate price to feel like the normal price for a specific audience or context.
# Products
A `Product` is the catalog aggregate in Openfront Ecommerce. It has title, description, handle, subtitle, gift-card/discount flags, thumbnail, status and metadata plus relationships to variants, options, images, categories, collections, tags, type, shipping profile, tax and discount records.
Variants and options [#variants-and-options]
`ProductVariant` is the sellable unit. Variants have titles and SKUs and connect to option values and money amounts. `ProductOption` and `ProductOptionValue` describe choices such as size or color without flattening them into arbitrary product JSON.
Prices [#prices]
Pricing is relational. Money amounts connect sellable variants to price sets and regional/currency context. Price lists and rules provide additional pricing policy. Store accepted price and total facts on order lines so a later catalog change does not rewrite an order.
Images and organization [#images-and-organization]
* `ProductImage` stores image URL and alt text.
* categories can form a hierarchy.
* collections group products for merchandising.
* tags and product types add classification.
* shipping profiles and tax relationships affect fulfillment and checkout policy.
Inventory [#inventory]
Inventory uses locations and stock movements rather than one marketing-page number. The exact available quantity depends on the variant, location, movements, reservations/fulfillment behavior and the operation reading it.
Current source does not justify blanket claims for CSV import/export, forecasting, automatic low-stock alerts, video galleries or every kind of digital/subscription/bundle product. Document those only when the corresponding model, workflow and test exist in your fork.
Operator path [#operator-path]
The dashboard has product list, create and detail routes plus category, collection and inventory pages. Use those feature screens for ordinary work and domain operations for price/inventory changes that need validation or audit. Generic list CRUD should not bypass order snapshots, tenant access or inventory accounting.
API example [#api-example]
The [GraphQL API page](/docs/openfront/ecommerce/api-reference) shows a query using the current generated field names. Always check `schema.graphql` from the same source revision as the deployment.
# Returns & Claims
Returns and claims cover the messy part of commerce that happens after the checkout is over.
Openfront treats those as structured workflows, not just support notes in an inbox.
Returns [#returns]
Returns are for the normal post-purchase case: the customer does not want the item anymore, sent the wrong size back, or needs a standard refund path.
That flow can include:
* return reasons
* inspection status
* restock decisions
* refund handling tied to payment providers
Claims [#claims]
Claims are for cases where something actually went wrong:
* damaged products
* defective products
* incorrect items
* shipping-related issues
Claims can include:
* uploaded images
* resolution tracking
* refund or replacement actions
* lifecycle status from opening to resolution
Why the distinction matters [#why-the-distinction-matters]
Not every return is a claim, and not every claim is just a refund.
Keeping those separate helps operations, reporting, and customer support stay clearer. It also makes it easier to automate the right next step instead of pushing everything through one generic after-purchase bucket.
In the dashboard [#in-the-dashboard]
Store staff can review active returns and claims from the order-management side of the dashboard and work through the records that need attention.
# Schema Visualizer
Database Schema [#database-schema]
Explore the complete Openfront database schema with this interactive visualization. Click and drag to navigate, zoom to focus on specific areas, and see how all the models connect together.
Understanding the Schema [#understanding-the-schema]
The Openfront database is built using Prisma with PostgreSQL and consists of several core model groups:
User Management [#user-management]
* **User** - The central user model with role-based permissions
* **Role** - Defines what actions users can perform
* **ApiKey** - API access tokens for programmatic access
E-commerce Core [#e-commerce-core]
* **Store** - Store configuration with currency and regional settings
* **Product** - Product catalog with variants, options, and collections
* **ProductVariant** - Individual product variations with pricing and inventory
* **Order** - Customer orders with full lifecycle management
* **Cart** - Shopping cart functionality with payment processing
Customer Management [#customer-management]
* **Address** - Customer addresses for billing and shipping
* **CustomerGroup** - Customer segmentation for pricing and discounts
* **User** - Customer accounts with order history and preferences
Payment & Financial [#payment--financial]
* **Payment** - Payment processing with multiple providers
* **PaymentSession** - Payment processing sessions
* **Currency** - Multi-currency support with conversion
* **MoneyAmount** - Flexible pricing system
Fulfillment & Shipping [#fulfillment--shipping]
* **Fulfillment** - Order fulfillment tracking
* **ShippingProvider** - Shipping integrations (Shippo, etc.)
* **ShippingLabel** - Shipping label generation and tracking
* **Region** - Geographic regions with tax and shipping rules
Advanced Features [#advanced-features]
* **Discount** - Flexible discount rules and promotions
* **GiftCard** - Gift card system with transactions
* **Return** - Return management with refund processing
* **Claim** - Product claim system with images
Key Relationships [#key-relationships]
The schema shows how Openfront provides a complete e-commerce platform:
1. **Store Structure**: Store → Products → Variants (product catalog)
2. **Order Flow**: Cart → Order → Fulfillment → Shipping (order processing)
3. **Payment**: PaymentSession → Payment → Capture/Refund (payment processing)
4. **Customer Journey**: User → Cart → Order → Return (customer lifecycle)
5. **Inventory**: ProductVariant → StockMovement → Location (inventory management)
This architecture allows Openfront to handle complex e-commerce scenarios with multi-currency, multi-region support, advanced pricing rules, and comprehensive order management.
# Shipping providers
Openfront's `ShippingProvider` model and `features/keystone/utils/shippingProviderAdapter.ts` support:
* local modules such as `shippo`, `shipengine`, and `manual`;
* independently deployed HTTP adapter apps configured per operation.
The five operation fields cover rate lookup, address validation, label creation, tracking, and cancellation. When a field begins with `http`, Openfront sends that route a JSON `POST` containing the provider and operation arguments. Otherwise, it imports the matching local file under `features/integrations/shipping`.
This configuration-driven route selection is intentional. It allows a merchant to connect a carrier, warehouse, aggregator, local courier, or custom shipping service without changing Openfront source.
Data boundary [#data-boundary]
Shipping profiles and options define store policy. Fulfillment providers, methods, labels, fulfillments, and fulfillment items record execution. The adapter translates the selected order, address, parcel, rate, or tracking context to its external service and returns a provider-neutral result.
The current callers query `accessToken` and send the selected provider object to the adapter. For an HTTP adapter, that means the configured app becomes a trusted credential and customer-data processor. Restrict route configuration, authenticate Openfront to the app, minimize forwarded fields, redact logs, validate responses, and apply outbound destination controls appropriate to the deployment.
External HTTP adapters are supported, but the current bridge supplies only a JSON `POST`: it has no built-in timeout, request signature, destination validation, or runtime response schema. Add those controls rather than replacing route adapters with a code-only registry.
Current source gaps [#current-source-gaps]
The backend operation fields are usable through a trusted administrative path, but both custom-provider UI paths are incomplete:
* the order-level form collects an API URL and stores it under `metadata.apiUrl` without assigning the operation fields;
* the provider drawer assigns the local token `custom`, but no matching `features/integrations/shipping/custom.ts` exists.
Do not claim a custom provider is connected merely because one of those forms creates a record. Verify the actual operation fields and execute each intended route.
Verification before enabling a provider [#verification-before-enabling-a-provider]
1. Confirm each configured operation destination and payload from current source.
2. Prove only an authorized operator can read credentials or change routes.
3. authenticate Openfront-to-adapter calls and reject replay where relevant.
4. validate order, address, parcel, line-item, rate, and provider relationships.
5. validate adapter JSON before writing fulfillment or label records.
6. make label purchase and cancellation idempotent.
7. reconcile uncertain provider outcomes before retrying.
8. deduplicate tracking updates and preserve operator recovery.
Manual fulfillment [#manual-fulfillment]
Manual shipping can record an operator-managed fulfillment without a carrier API. It should remain explicit and must not fabricate carrier tracking events.
Start with [Add a shipping adapter](/docs/openfront/ecommerce/how-to-guides/custom-shipping-provider).
# Store configuration
The current dashboard and storefront read the first `Store` record as shared application configuration. The operator page is available at `/dashboard/platform/store`.
Current Ecommerce source behaves as a single-store application. Although the schema can hold Store records and includes organization-related models elsewhere, products, carts, orders, regions, and other core aggregates do not form a complete organization-partitioned tenant graph.
Dashboard-managed settings [#dashboard-managed-settings]
The Store settings screen currently reads and updates:
* store name;
* logo SVG;
* logo color;
* homepage title;
* homepage description.
The server action sanitizes submitted logo SVG with SVGO before saving it. Updates require the backend permission used to manage sales-channel settings; hiding or showing the dashboard page is not the authorization boundary.
Additional Store fields [#additional-store-fields]
The Keystone `Store` model also contains:
* `defaultCurrencyCode`;
* related currencies;
* payment, swap, and invite link templates;
* JSON metadata;
* a computed payment-provider configuration derived from installed providers and public Stripe or PayPal environment values.
Those fields are part of the GraphQL model, but they are not all editable in the current Store settings screen. Use the generated schema from the same revision before building another administrative client.
Currency and market configuration [#currency-and-market-configuration]
The Store record is not the complete market model. Regions, countries, currencies, prices, tax rates, payment providers, and shipping options have their own records and workflows. Configure and test those relationships separately rather than assuming that changing `defaultCurrencyCode` converts prices or establishes a security boundary.
Operational checks [#operational-checks]
Before publishing configuration changes:
1. verify the dashboard permission and API access path;
2. preview branding on the built-in storefront at desktop and mobile sizes;
3. confirm each region's currency, prices, countries, tax, payment, and shipping behavior;
4. validate link templates against an allowlisted application origin;
5. keep private provider credentials out of Store metadata and public environment variables;
6. test rollback for a malformed logo or template.
For independent operator clients, use a bounded Store projection and an allowlisted update input. Do not expose unrestricted generic Store mutation access to a browser.
# What is Openfront Ecommerce?
Openfront Ecommerce is a self-hosted, single-Store commerce application built from Next.js, Keystone, GraphQL, Prisma, and PostgreSQL. The operator owns the source, database, storefront and customer relationships. Regions, countries, currencies, locations, and sales channels configure commerce behavior; they are not proven independent tenant boundaries.
It also establishes application patterns used by other Openfront products. Restaurant, Grocery, Hotel, Construction and other products reuse its source/data ownership and feature boundaries while replacing commerce-specific models with their own domain records.
Main surfaces [#main-surfaces]
Operator dashboard [#operator-dashboard]
The dashboard has feature-owned pages for products, orders, inventory, fulfillment, regions/currencies/countries, payments, shipping, discounts, gift cards, price lists, stores, users, API keys, apps, invoices, business accounts and system settings.
Storefront [#storefront]
Country-code routes provide home, store, product, category, collection, cart, checkout, confirmation and account journeys. Storefront clients should use narrow data operations and ownership-checked cart/order workflows rather than unrestricted private lists.
Payment and fulfillment [#payment-and-fulfillment]
Payment and shipping provider models and adapter source exist. Provider records are configuration, not proof of live settlement or shipping. Before enabling an adapter, verify environment secrets, server-derived totals, signed webhooks, replay handling, refunds, failures and reconciliation in the deployed environment.
Before using real data [#before-using-real-data]
Review the source-specific security and limitation notes on each workflow page before using real customer, provider, or payment data.
# External dashboards
To create a custom Grocery dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Grocery's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Grocery codebase before proposing work.
# External storefronts
To create a custom Grocery storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Grocery's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Grocery codebase before proposing work.
# Openfront Grocery
[Source](https://github.com/openshiporg/openfront-grocery) · [Catalog](https://openship.org/products/openfront-grocery)
Openfront Grocery is a single-store grocery storefront and operator platform. It combines departments and fresh catalog data, carts, coupons, substitutions, delivery and curbside capacity, shopping lists, subscriptions, supplier purchasing, lot inventory, fulfillment, Stripe payment evidence, refunds, and an operational outbox.
Current source is not a self-contained multi-store bootstrap. First-user initialization does not create/assign a Store, while `runGroceryOnboarding` requires the signed-in user to already have one. Complete and verify that ownership step on an isolated database before running onboarding. Do not infer organization/location tenancy, payment settlement, supplier execution, or delivery execution from the current records.
Customer and operator surfaces [#customer-and-operator-surfaces]
Customer routes cover `/`, departments and product detail, `/cart`, `/checkout`, `/deals`, `/lists`, `/subscriptions`, `/account`, and `/order/[id]`. A shopper can manage an owned guest/account cart, substitution preferences, coupons, lists and recipes, a pickup or delivery window, subscriptions, and pickup check-in.
Purpose-built operator routes are:
* `/dashboard/platform/orders` and `/fulfillment` for picking, substitution, packing, handoff, and delivery state;
* `/dashboard/platform/delivery` and `/pickup` for slots, capacity, routes, parking, check-in, and handoff;
* `/dashboard/platform/inventory` for lots, expiry, quantity, and adjustments;
* `/dashboard/platform/purchasing` and `/suppliers` for purchase-order drafts, transitions, and receipt;
* `/dashboard/platform/merchandising` for departments/products/coupons;
* `/dashboard/platform/customers` and `/subscriptions` for account and recurring-order operations.
Thin route wrappers delegate to `features/storefront`, `features/platform`, and the Keystone mutation/projection layer. Generic list administration remains available only under its permissions.
Schema and store boundary [#schema-and-store-boundary]
The graph includes Store and singleton StoreSettings, users/roles, departments/products, suppliers and purchase orders, lots/adjustments, carts/items, orders and immutable lines, substitutions, delivery/pickup/parking/route records, coupons/loyalty/subscriptions/lists/recipes, providers/sessions/payments/refunds/webhook events, and GroceryOutboxEvent.
The current boundary is one active Store per user/session:
* `User.store` is required in the current model/migration state;
* `requireSessionStore()` resolves the current user's store for protected commands;
* public reads select the first active Store;
* operational records carry direct Store relationships and custom mutations recheck them;
* `StoreSettings` remains singleton and is not itself related to Store;
* current backfills use fixed identity `store_juniper`.
This is not an organization or location tenant graph. Model-level access remains role-oriented in several places, so custom command checks and relationship constraints are material. Raw Cart/CartItem writes are operator-only, raw Order lifecycle fields are restricted, and immutable order lines cannot be freely updated.
Catalog pages currently use field-access-controlled generated Product and Department queries rather than bespoke DTOs for every read. Product field access protects cost, low-stock, supplier, and inventory relationships, but those field names remain in the generated schema. Guest order access uses a token-checked resolver returning the Order type. Describe this as controlled raw catalog reads plus named operational projections—not a fully projection-only public API.
Controlled GraphQL boundary [#controlled-graphql-boundary]
Customer/public queries include `groceryCart`, `clippedCoupons`, `scaleRecipe`, `activeCartPaymentProviders`, `guestGroceryOrder`, `publicGroceryCoupons`, `publicGroceryAvailability`, pickup-slot queries, and parking availability.
Named customer commands cover:
* add/update/remove/clear/merge cart and substitution preference;
* clip/unclip/preview/apply coupons;
* create/update/pause/resume/cancel/skip subscriptions;
* create/manage shopping lists and add lists/recipes to cart;
* reserve/release pickup capacity, check in, release parking, and complete handoff;
* `initiatePaymentSession` and `submitGroceryOrder`.
Operator commands include delivery-route creation/transition, `advanceOrderFulfillment`, substitution recording, `refundPayment`, capacity configuration, lot adjustment, purchase-order draft/remove/transition/receive, and outbox claim/complete/replay/status.
Named task projections are `groceryPlatformOrders`, `groceryPlatformFulfillment`, `groceryPlatformDelivery`, `groceryPlatformPickup`, `groceryPlatformInventory`, `groceryPlatformSuppliers`, `groceryPlatformPurchasing`, `groceryPlatformMerchandising`, `groceryPlatformCustomers`, and `groceryPlatformSubscriptions`.
Checkout re-reads product, lot, slot, provider session, amount, and settlement state; allocates eligible lots in expiry order; and writes order/payment/fulfillment/outbox evidence with immutable line snapshots. Those source checks still require runtime concurrency and rollback tests against the target PostgreSQL configuration.
Onboarding and local data [#onboarding-and-local-data]
`runGroceryOnboarding` uses an advisory lock and serializable transaction to create synthetic Juniper settings, departments, suppliers, products, lots, fulfillment capacity, providers, coupons, loyalty, a customer/address/list/subscription, and sample orders/payments. It marks the current user complete and includes repeat/failure-injection logic.
It does not create the Store required by `requireSessionStore()`. On a clean database, first establish and assign a Store through an approved owning-source path, then run onboarding twice and verify stable tenant/catalog relationships. Seeded customers, addresses, prices, orders, and payment records are fictional local data, not a hosted demo or operating evidence.
Payments and integrations [#payments-and-integrations]
The current adapter registry exposes Stripe under `pp_stripe_default`. A manual module exists, but it is not registered; the seeded manual row is disabled/operator-only and cannot be described as an available checkout adapter.
The Stripe adapter implements intent create/capture/refund/status/link and raw-body signature verification. Webhook ingress is `/api/payments/webhooks/[providerCode]`; the handler verifies the adapter signature before privileged work, claims replay identity, updates matching payment evidence, and records PaymentWebhookEvent. Provider source/configuration is not proof of authorization, capture, refund, or reconciliation.
Supplier purchasing, tax, loyalty accounting, outbound delivery, notifications, and outbox dispatch have no provider execution implied by their rows. Add each effect behind typed allowlisted adapters, encrypted/denied credentials, signed ingress, idempotency, durable retries, reconciliation, and operator exception handling.
Deployment and current limitations [#deployment-and-current-limitations]
Recent migrations add refund relationships, retain Store ownership, convert catalog identity links to relations, and enforce non-null Store/evidence constraints. They include retained-data failure checks and depend on the preceding `store_juniper` backfill. Review and apply the full ordered history; do not cherry-pick the final constraints.
Both the npm `dev` and `build` scripts run migration deployment before Next.js. Railway instead builds without migration and migrates at start. Use deliberate database credentials, separate migration from immutable build where needed, supply real S3/payment secrets, back up/restore-test the target, and run current schema, type, build, ownership, checkout, refund, substitution, capacity, inventory, procurement, webhook, and outbox tests.
Current source does not establish multi-store isolation, organization/location tenancy, complete first-store onboarding, a registered manual provider, provider settlement, delivery/fleet execution, supplier transmission, tax calculation, durable outbox delivery, or production-load inventory safety. Do not use real customer/payment data until those exact release and deployment boundaries have been independently verified.
# External dashboards
To create a custom Gym dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Gym's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Gym codebase before proposing work.
# External storefronts
To create a custom Gym storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Gym's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Gym codebase before proposing work.
# Openfront Gym
[Source](https://github.com/openshiporg/openfront-gym) · [Catalog](https://openship.org/products/openfront-gym)
Openfront Gym is an organization- and location-scoped membership operations product. It combines public class/facility/instructor discovery, membership signup and account management, class capacity and waitlists, trainer appointments, member and kiosk check-in, billing evidence, and operator reporting.
The onboarding UI performs dependency-ordered generated mutations and tolerates some class-instance and demo-booking failures; it is not one atomic backend onboarding command. Completion checks only a minimum settings/location/tier/class/instructor/schedule/instance set. Inspect partial data before using the workspace.
Member, kiosk, and operator surfaces [#member-kiosk-and-operator-surfaces]
Public/member routes include `/classes`, `/schedule`, `/memberships`, `/join`, `/instructors`, `/facilities`, `/contact`, `/account`, and `/portal`. Member routes expose bookings, membership/profile, instructor work, and a QR/check-in code.
Kiosk routes are `/kiosk` and `/kiosk/check-in`, backed by authenticated API routes for member search, member/guest check-in, QR code, and profile. The HTTP kiosk path validates the kiosk credential and member/membership state before persisting a capacity-controlled CheckIn. The legacy GraphQL `kioskCheckIn` operation performs lookup and returns attendance identity but does not itself create the CheckIn row; integrations should not confuse the two paths.
Operator routes cover billing, scheduling, front-desk check-in, settings, reports, locations, members, membership plans, instructors, class catalog, and rosters under `/dashboard/platform/*`.
Schema and tenancy [#schema-and-tenancy]
`Organization` is the tenant root in current source. The authenticated session carries one organization; operational models use required organization relationships and tenant filters/ownership checks. Locations, resources, trainer availability, and trainer appointments add location scope. The current source does not expose a multi-organization switcher, even though Organization records can describe multi-location use.
The graph includes members, tiers/memberships/subscriptions, gym/membership payments, methods/providers/sessions/events, locations/settings, exercises/workouts, attendance/check-ins, class types/schedules/instances/bookings/waitlists, instructors, resources, trainer availability, and appointments.
The latest tenant migration creates Organization, GymResource, TrainerAvailability, and TrainerAppointment, backfills legacy rows to `gym_default_organization`, then enforces required organization relations and indexes. Review retained-data ownership before applying those constraints; the backfill does not prove that legacy rows belonged to one real organization.
Controlled queries and commands [#controlled-queries-and-commands]
Purpose-built public projections include `publicGymSettings`, class type/schedule/instance collections and detail, instructor collections/detail, and membership-tier collections/detail. They do not expose member or payment records.
Named operations include:
* `checkClassAvailability`, `bookClass`, `cancelClassBooking`, and `promoteFromWaitlist`;
* `checkIn`, `recordMemberCheckIn`, `checkOutMember`, `markClassAttendance`, and the legacy lookup-oriented `kioskCheckIn`;
* `bookTrainerAppointment` and `transitionTrainerAppointment`;
* `upsertGymSettings` and `getBillingStats`;
* `initiateMembershipCheckout`, setup intent, billing portal, membership cancel/freeze/unfreeze/tier change, recovery-contact recording, and `refundGymPayment`.
Class capacity and lifecycle code rechecks tenant/member ownership and expected state in controlled operations. Prove simultaneous booking, cancellation/promotion, check-in, and trainer/resource conflicts against the deployed PostgreSQL isolation and retry behavior.
`markPaymentRecoveryContacted` records staff follow-up; it is not an automated collection or payment-recovery service.
Onboarding and synthetic data [#onboarding-and-synthetic-data]
The current browser/server action marks onboarding in progress, creates settings, location, tiers, class types, instructor users/profiles, schedules, up to fourteen days of instances, synthetic members/memberships/bookings, exercises/workouts, providers/methods/subscriptions/payments, then marks complete after minimum-count checks.
Instance-creation and demo-booking conflicts can be caught and skipped. A completed status therefore does not guarantee that every optional demo record exists. Run onboarding only against an isolated database, inspect each required relationship and failed step, then test member signup, booking/waitlist, persisted kiosk check-in, trainer appointments, portal ownership, and operator permissions. Seeded Stripe IDs, memberships, receipts, and access codes are fictional and do not create a hosted demo.
Stripe and provider boundary [#stripe-and-provider-boundary]
The registered adapter set is Stripe plus a test mode. `PAYMENT_TEST_MODE=true` can select test behavior for the Stripe-keyed boundary. The active membership join path calls `initiateMembershipCheckout`; the old `/api/stripe/create-checkout-session` route returns HTTP 410 and must not be used.
Current Stripe source covers hosted subscription checkout, setup intents, billing portal, cancellation/freeze/resume/tier changes, refunds, and raw-body signed webhooks at `/api/stripe/webhook`. Provider credentials are write-only and adapter keys are constrained to server loaders. These controls do not establish provider account approval, correct prices, successful settlement, refund finality, dispute handling, or reconciliation.
No PayPal or other production provider is registered. The QR/check-in path is local membership evidence, not identity proof, physical door control, insurance verification, or medical clearance.
Deployment and limitations [#deployment-and-limitations]
The npm `dev` and `build` scripts deploy migrations before running Next.js; Railway builds without migration and migrates when the app starts. Separate migration from immutable builds where needed, protect kiosk secrets, use managed Stripe/session/storage secrets, review the organization backfill, and run current schema/type/tests plus responsive member/operator/kiosk flows.
Before real operations, verify cross-organization and cross-member denial, location/resource ownership, simultaneous class and trainer booking, waitlist promotion, kiosk replay, signed webhook duplication, server-derived tier amount/currency, membership transitions, refunds, restore, monitoring, and incident response.
Current source does not provide non-Stripe provider coverage, automated collections, physical access-control guarantees, payroll, insurance processing, medical-safety decisions, or proof of production-load capacity behavior. Do not describe source tests, seed data, or a rendered portal as settlement, identity, safety, accessibility, privacy, or operational certification.
# External dashboards
To create a custom Hospital dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Hospital's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Hospital codebase before proposing work.
# External storefronts
To create a custom Hospital storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Hospital's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Hospital codebase before proposing work.
# Openfront Hospital
[Source](https://github.com/openshiporg/openfront-hospital) · [Catalog](https://openship.org/products/openfront-hospital)
Openfront Hospital is a self-hosted healthcare operations foundation with public provider discovery and booking, intake, patient/clinician records, schedules, communications, and clinical work.
This project is not documented as HIPAA compliant, certified clinical software, an EHR, or a medical device. Do not use it for real patient or clinical data without a separate security, privacy, compliance, hosting, and operational program.
Architecture and schema [#architecture-and-schema]
`Facility` is the practical operating scope, but current source has no dedicated facility-membership tenant table. Staff/provider/patient relationships derive access and one explicit role capability can access all clinical data; this is narrower than a full organization-membership boundary. The active schema includes facilities, departments, rooms/wards/beds, providers and clinicians, appointment types and schedule blocks, patients, portal access, intake forms, appointments, messages, admissions, encounters, care plans, orders, prescriptions, labs, procedures, claims, consent, vital signs, and clinical audit events.
Public routes are `/`, `/providers`, `/book`, `/book/[slug]`, and tokenized `/intake/[accessCode]`. Operator routes under `/dashboard/platform` cover registration, appointments, intake, schedules, patients, care queue, inpatient work, clinical operations and fulfillment, communications, claims, and reports.
Main workflow [#main-workflow]
A configured facility publishes providers and appointment types. A patient chooses a slot, submits limited booking information, and receives a controlled intake path. Staff review appointments and intake before creating or updating clinical records through authenticated operations. Consent and clinical audit records are intended as append-only evidence.
Bounded GraphQL operations [#bounded-graphql-operations]
Named clinical operations register patients; arrive appointments; manage care-queue and admission/bed readiness; create and transition encounters, orders, prescriptions, labs, procedures, claims, and care plans; correct lab results; and dispatch clinical outbox work. Tokenized intake uses a dedicated bounded query. These workflow operations do not make a clinical decision safe or compliant; authorized clinicians and release-specific policy remain responsible.
Setup and onboarding [#setup-and-onboarding]
Use a disposable PostgreSQL database with synthetic data only. Hospital onboarding seeds Northstar Family Care facilities, departments, providers/clinicians, appointment types/schedules, patients, appointments, intake, messages, and portal access. The seed includes a known literal intake code (`demo-rivera-intake`); it is deliberately discoverable evaluation data and must never be copied into a real patient path. Run onboarding and current schema/static/security/public-flow checks against an isolated database, then test facility/provider/patient scope, access-code expiry/revocation, and clinical lifecycle denial.
Integrations [#integrations]
The current product does not claim direct patient payment and has no `features/integrations` adapter registry. Email/SMS, labs, insurance, identity, records exchange, file storage, portal invitations, or external clinical systems are not implied by corresponding data/workflow fields. Document an adapter only when its execution, authentication, retries, reconciliation, and failure handling are tested.
Security and deployment [#security-and-deployment]
Patient, intake, encounter, message, consent, audit, insurance, prescription, and lab records need facility/patient/provider scoping and sensitive-field denial. Public access codes must be high-entropy, hashed, expiring, and revocable. Railway configuration builds then migrates at start but is not deployment proof.
Deployment needs reviewed migrations, encryption/key management, backup restoration, access review, audit retention, breach response, monitoring, and jurisdiction-specific privacy/clinical policy beyond this repository. Current source does not establish EHR interoperability, clinical decision support/correctness, billing/RCM, real provider delivery, PHI hosting controls, medical-device status, HIPAA compliance, or any healthcare certification.
# External dashboards
To create a custom Hotel dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Hotel's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Hotel codebase before proposing work.
# External storefronts
To create a custom Hotel storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Hotel's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Hotel codebase before proposing work.
# Openfront Hotel
[Source](https://github.com/openshiporg/openfront-hotel) · [Catalog](https://openship.org/products/openfront-hotel)
Openfront Hotel is a single-property hotel/PMS-style application. It combines room-night inventory, rates and direct booking, token-bound guest access, front-desk and reservation work, folios, payments, housekeeping, maintenance, channel records, group blocks, business date/night audit, audit, and outbox evidence.
Current source has no full multi-property tenant graph or native Booking.com/Expedia adapter. HotelSettings is a singleton, most records are database-global, and literal `the-alder-house` property keys scope selected jobs/evidence only. A channel row or successful local sync event can exist without any remote endpoint call.
Guest and operator workflows [#guest-and-operator-workflows]
Guest routes cover `/rooms`, room detail, `/book`, `/booking/[id]`, `/bookings/lookup`, `/account`, amenities, location, contact, and PayPal return. The guest can search availability/quotes, create a booking under a room-inventory transaction, establish access using confirmation number/email, receive a signed cookie backed by a hashed token, review an owned booking, initiate payment, cancel, or request modification.
Operator routes include `/dashboard/platform/front-desk`, reservations, rooms, rate plans, guests, payments/providers, folios, channels, housekeeping, maintenance, and analytics. Named actions assign rooms, update stay/status, manage housekeeping and maintenance, post/reverse folio entries, run night audit, create/pick up groups, and replay failed outbox work.
Schema and property boundary [#schema-and-property-boundary]
The graph includes HotelSettings, room types/rooms/images, rates and seasonal rates, room inventory, guests/documents, bookings and immutable reservation lines, assignments, providers/sessions/payments/events, folios/entries, channels/reservations/sync events, housekeeping, maintenance, loyalty, daily metrics, business dates/night audits, groups/allocations, audit events, and outbox events/attempts.
There is no Organization, Property, or Location ownership relationship across those aggregates. Role permissions are global within the database. Newer audit/outbox/night-audit records carry `propertyKey`, but that does not filter every booking, guest, room, payment, or channel. Deploy one property per database unless the owning source adds direct property ownership and cross-property tests throughout.
Booking create/update/delete and reservation-line writes are denied through raw model operations. Controlled commands snapshot room, rate, plan, dates, guests, currency, and pricing so later configuration changes do not silently rewrite accepted reservation facts. Some legacy Booking monetary fields remain Float dollars while snapshots, folios, and payment paths use integer minor units; the schema is not uniformly fixed-point.
Bounded GraphQL boundary [#bounded-graphql-boundary]
Public projections include `publicHotelSettings`, `bookingPaymentProviders`, `activeBookingPaymentSession`, `storefrontRoomTypes`, `storefrontRoomType`, `storefrontAvailability`, `storefrontQuote`, `guestBooking`, and `guestBookings`. Guest queries recheck the signed cookie, booking IDs, and email rather than returning arbitrary reservations.
Named mutations include:
* guest access, `createStorefrontBooking`, modification request/resolution, and cancellation;
* booking status, room assignment, stay-date change, and snapshot repair;
* payment-session initiation/completion and operator payment recording;
* folio posting and exact reversal;
* room operational state, housekeeping, maintenance, inventory controls, and rate publication;
* channel inventory push/reservation pull/retry;
* onboarding, night audit, outbox replay, and group create/pickup/status.
Availability and booking source uses serializable room-inventory work, but runtime oversell, retry, and deadlock behavior must be tested under target load. Guest tokens and cookies need expiry, rotation, guessed-booking, wrong-email, wrong-cookie, and revocation tests.
Onboarding and synthetic data [#onboarding-and-synthetic-data]
`runHotelOnboarding` uses an advisory lock and transaction. Minimal/full templates create the singleton settings, rooms/rates/inventory, guests/bookings/payments, housekeeping/maintenance, channel records/events, loyalty, metrics, snapshots, guest access, and folios; `custom` currently normalizes to minimal.
Full data includes Booking.com/Expedia-looking rows with demo credential metadata, manual/offline payments, and fictional guests/bookings. Those records exercise local screens only. Run onboarding twice on an isolated database and test stable identity, booking/folio relations, room search, concurrent booking, token access, payment/cancellation/refund, front-desk assignment, housekeeping, night audit, group folio routing, and protected routes.
Payments, channels, and workers [#payments-channels-and-workers]
Static payment loaders register Stripe and PayPal for customer payment plus manual/operator-recorded ledger payments. Manual is excluded from customer checkout. Stripe implements PaymentIntent/refund/status and raw-body signature checks; PayPal implements order create/capture/refund/status and calls PayPal's webhook-verification API. Ingress is `/api/payment-providers/[providerCode]/webhook` and claims replay identity before payment finalization.
Channel integration is generic configured HTTP, not native OTA source. `/api/webhooks/channel/[channelId]` verifies configured HMAC and provider event identity before reservation mapping. `pushInventoryToChannel` can record local success when no remote endpoint is configured, and `pullReservationsFromChannel` can return an empty list and record success in the same condition. Treat those as no-op local outcomes, never proof of OTA acceptance or synchronization.
Channel polling jobs run in the application process only when `CHANNEL_SYNC_JOBS_ENABLED=true`. Outbox delivery jobs likewise require `HOTEL_OUTBOX_JOBS_ENABLED=true`, a dispatch URL, and a dispatch secret. Otherwise events remain pending. Review duplicate-job behavior before horizontally scaling the web process; a dedicated worker deployment is safer than assuming every app instance should poll.
Email/confirmation source, worker loops, provider rows, local webhook responses, or pending outbox records do not establish guest notification, remote channel exchange, payment settlement, or reconciliation.
Deployment and current limitations [#deployment-and-current-limitations]
The npm `dev` and `build` scripts deploy migrations before running/building Next.js. Railway builds without migration and runs migration at application start. Recent migrations enforce required/restricted relationships, business date and immutable night audit, group allocation and folio routing, and outbox leases/attempt/retry/dead-letter fields. Review retained data before applying these constraints.
For release, back up and restore-test the target, apply reviewed migrations separately, configure provider/channel/worker secrets, run current tests/type/build, and exercise single-property access, booking concurrency, guest tokens, room assignment, folio balance/reversal, night audit, group pickup, payment webhook replay, channel no-endpoint behavior, outbox retries, and multi-process jobs.
Current source does not establish multi-property isolation, native OTA connectivity, uniform minor-unit money, guaranteed email delivery, external worker supervision, payment/channel acceptance, or hotel regulatory, PCI, privacy, identity, accessibility, or operational certification. Do not use real guest, document, or payment data until those exact deployment controls are independently verified.
# External dashboards
To create a custom Law Firm dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Law Firm's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Law Firm codebase before proposing work.
# External storefronts
To create a custom Law Firm storefront, copy the skill and give it to your LLM. The skill will first explain that current Law Firm source has no canonical `features/storefront` layer, then inspect the actual public or portal routes and identify any backend contracts needed before treating them as a storefront.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Law Firm codebase before proposing work.
# Openfront Law Firm
[Source](https://github.com/openshiporg/openfront-law-firm) · [Catalog](https://openship.org/products/openfront-law-firm)
Openfront Law Firm is an open-source legal-practice operations application. It gives a firm one data model for intake, conflicts, matters, calendars, communications, documents, billing, payments, trust records, client sharing, integrations, retention evidence, and controlled agent work.
The software supplies configurable controls and evidence. It does not guarantee legal-ethics, privilege, trust-accounting, privacy, retention, signature, security, or jurisdiction-specific compliance. Each deployment still needs qualified firm policy, legal, security, and provider review.
Install and configure [#install-and-configure]
The application uses Node.js, Next.js, Keystone, Prisma, and PostgreSQL. Install dependencies from the repository root:
```bash
npm install
```
Configure a local PostgreSQL database and independent secrets:
```bash
DATABASE_URL=postgresql://user:password@127.0.0.1:5432/runtime_law_firm?schema=public
SESSION_SECRET=
MACHINE_CREDENTIAL_PEPPER=
INTEGRATION_CREDENTIAL_KEY=
OPENFRONT_PUBLIC_ORIGIN=https://law-firm.local
OPENFRONT_INTERNAL_ORIGIN=https://law-firm.local
```
The machine-credential pepper and integration-encryption key must remain stable, separate, and outside source control. Production requires explicit public and internal HTTPS origins; authenticated internal requests do not derive their destination from forwarded host headers.
Compile the schema and application without touching the database:
```bash
npm run schema:build
npm run typecheck
npm test
npm run lint
npm run build:app
```
Do not use `prisma db push` or a destructive reset. Schema changes are represented by reviewed migrations.
Architecture [#architecture]
`Firm` is the tenant root. Offices, memberships, and firm-specific roles establish ordinary operator access. Matters can add narrower team assignments and ethical walls. Keystone list definitions under `features/keystone/models` are authoritative; `schema.graphql` and `schema.prisma` are generated evidence.
Every non-global domain record carries a firm relationship even when ownership could be inferred through a parent. Matter access uses controlled `authorizedUsers` and `blockedUsers` relations derived from team and ethical-wall evidence. Raw clients cannot edit those authorization indexes.
Money uses integer minor units and explicit currency codes. Issued invoice lines, document versions, conflict dispositions, matter events, trust transactions and entries, signature events, webhook attempts, retention actions, audit events, and agent-action events preserve history through append-only records or compensating operations.
Data model [#data-model]
* **Firm operations:** firms, offices, users, roles, memberships, contacts, addresses, and contact methods.
* **Intake and conflicts:** intakes, participants, submissions, consultations, conflict checks, terms, matches, and human dispositions.
* **Matters and work:** practice areas, matters, parties, teams, ethical walls, courts, dockets, entries, deadlines, and tasks.
* **Communications and documents:** threads, participants, messages, deliveries, documents, immutable versions, signature requests, signers, and events.
* **Billing and trust:** rate cards and rules, time, expenses, retainers, invoices and lines, payments, allocations, refunds, trust accounts, client/matter subledgers, transactions, entries, snapshots, and reconciliations.
* **Portal and integrations:** portal accounts and exact matter/resource grants, provider connections, external record links, webhook subscriptions, deliveries, and attempts.
* **Governance and agents:** retention policies, holds and actions, audit events, API keys, OAuth clients and tokens, agent principals, credentials, grants, actions, and action events.
Core relationships are explicit rather than hidden in JSON. Provider payloads and metadata are bounded supplements, not the only representation of a matter, balance, payment, document, or authorization decision.
Workflows [#workflows]
Sensitive state changes use named GraphQL operations instead of raw status writes.
* First-firm onboarding atomically creates the firm, office, administrator role, membership, practice area, and active-firm assignment. Authenticated users without a firm are routed to `/dashboard/onboarding`.
* Conflict operations run a check, record a reviewer disposition, and finalize the result before a matter can proceed.
* Matter-team and ethical-wall operations update policy evidence and materialized access together, including overlapping-wall reconciliation.
* Invoice and payment operations control approval, issue, capture, allocation, exact allocation reversal, independently reviewed refunds, and provider outcomes.
* Trust posting requires one firm, account, currency, balanced positive minor-unit entries, serializable execution, idempotency, and exact reversal rather than history edits.
* Portal operations invite a contact, grant exact matter capabilities, share exact resources, and revoke access without broad list reads.
* Provider operations rotate encrypted credentials and record communication, signature, connection, and webhook state transitions.
* Agent operations require an active principal, credential, exact-firm and optional exact-matter grant, bounded capability, idempotent request, and immutable result evidence. Consequential actions can require digest-bound human approval.
Purpose-built operator routes now cover intake, contacts, matters/work, communications, documents, time and expenses, billing, accounting, portal administration, integrations, governance, data operations, and reports. Generated list administration and onboarding remain available. Public intake and a client-facing portal UI are not supplied by current source.
Bounded GraphQL operations [#bounded-graphql-operations]
Firm and practice workspace queries return scoped operational projections. Named operations handle onboarding, intake/conflict and matter access, document and communication evidence, invoice/payment/refund and trust posting, portal invitations and exact-resource sharing, provider/webhook state, retention holds, API/OAuth credentials, and controlled agent requests/execution. Raw status, money, access indexes, credentials, and immutable evidence stay outside ordinary GraphQL writes.
Synthetic local workflow [#synthetic-local-workflow]
The repository includes a deterministic local-only seed. It creates an administrator, firm, office, practice area, synthetic contact, matter, conflict check, invoice, and payment. The guard accepts only the loopback database named `runtime_law_firm`:
```bash
export MIGRATION_CONFIRM_DATABASE=runtime_law_firm
export ALLOW_LOCAL_DEMO_SEED=runtime_law_firm
npm run seed:runtime
```
Rerunning the seed reuses its fixture IDs. `npm run verify:complete-runtime` creates additional synthetic records and exercises restricted-matter access, conflicts, billing, refunds, portal grants, provider records, retention, API keys, OAuth, and controlled agents. It must be explicitly enabled with `ALLOW_RUNTIME_PROOF=runtime_law_firm` and must not target real firm data.
Integrations [#integrations]
The application implements records and controlled transitions for provider connections, encrypted credential envelopes, external record links, communication deliveries, signatures, and webhooks. It also implements API-key authentication, OAuth authorization code with S256 PKCE, refresh rotation, and grant-scoped agent credentials.
Those boundaries do not send real email or SMS, store real documents, collect real payments, calculate court deadlines, submit court filings, or obtain signatures by themselves. Each provider needs a separate adapter, inbound verification, retry and reconciliation policy, sandbox certification, least-privilege credentials, and deployment monitoring.
Security [#security]
Access fails closed when the user, firm, membership, role, active firm, machine scope, matter assignment, or ethical-wall decision does not authorize the request. Restricted matters require explicit team access; a firm administrator does not silently bypass an ethical wall. Credential material is omitted from GraphQL reads.
The HTTP GraphQL boundary accepts POST requests, requires JSON for ordinary calls, restricts multipart requests to the configured public origin, bounds bodies and uploads, caps list results, limits query depth and self-reference, disables HTTP introspection, and redacts internal error extensions. Portal sessions receive exact resource shares rather than general list access.
These application controls do not replace managed key storage, centralized rate limits, logging and alerting, incident response, penetration testing, backup restoration, provider review, or firm-specific legal and security decisions.
Deployment [#deployment]
The repository includes four current migrations and a guarded script for its maintained **local synthetic** database. For an existing local runtime, the guard requires the exact loopback target, matching migration history and checksums, no interrupted migration, a recent PostgreSQL custom-format backup, its SHA-256 reference, and an advisory deployment lock:
```bash
export MIGRATION_CONFIRM_DATABASE=runtime_law_firm
export MIGRATION_BACKUP_PATH='/absolute/path/to/pre-deploy.dump'
export MIGRATION_BACKUP_REFERENCE='sha256:'
npm run migrate:maintained
```
That command is deliberately limited to the named loopback synthetic runtime and is not a general deployment procedure. For any release, run the current schema build/runtime verification, test suite, lint, typecheck, application build, migration-history/checksum checks, authenticated firm/matter/portal workflows, GraphQL boundary negatives, and browser checks against the exact source snapshot and target configuration.
A deployment for real firm data needs its own reviewed migration and restore procedure, managed secrets and encryption keys, implemented and tested provider adapters, monitoring, centralized rate limits, incident response, staged rollout and rollback, retention policy, and jurisdiction-specific legal review. Current source does not itself send communications, store document bodies in an object service, collect payments, submit filings, obtain signatures, or guarantee trust-accounting or deadline-rule compliance.
Extension paths [#extension-paths]
* Add or change domain records in `features/keystone/models`, register every list explicitly in `features/keystone/models/index.ts`, generate the schemas, and add a reviewed migration.
* Put sensitive lifecycle changes in a narrow operation under `features/keystone/mutations`; do not reopen raw writes to protected status, money, access, credential, or evidence fields.
* Add provider adapters behind `IntegrationConnection` and the provider workflow boundary. Keep credentials in encrypted envelopes, verify callbacks, claim provider event IDs, and record attempts and reconciliation evidence.
* Build operator pages under `app/dashboard` and separate public intake or portal routes from the generated administrator lists. Public and portal surfaces should call narrow operations, not raw legal-domain CRUD.
* Add machine capabilities by mapping explicit API, OAuth, or agent scopes to existing domain permissions and exact tenant/matter checks. High-risk agent actions need immutable requests and human approval where policy requires it.
* Extend tests with cross-firm, restricted-matter, ethical-wall, credential, lifecycle, idempotency, concurrency, and failed-provider cases before enabling a new path.
# Marketplace architecture and workflows
Route and feature structure [#route-and-feature-structure]
The public route at `/` owns the conversational marketplace screen; `/ethos` explains the interoperability model. `/api/completion` runs built-in AI chat, and `/api/mcp-transport/[transport]` exposes the MCP server. The thin route files delegate to `features/marketplace` server and screen modules.
There is no local commerce database. `marketplace.config.json` is the reviewed store registry. Connected merchant APIs remain the system of record.
Store registry and outbound requests [#store-registry-and-outbound-requests]
Each store has a stable ID, root HTTPS origin, implemented platform identifier, and optional display metadata. Request-time parsing rejects credentials, queries, fragments, nonstandard ports, local/reserved names, non-public IPv4/IPv6 answers, and unsupported adapters. Every DNS answer must be public. The selected public address is pinned to the request socket, TLS hostname checks remain active, responses and timeouts are bounded, and redirects are refused.
Buyer input selects a known store ID; it does not select an endpoint. This is the critical distinction between a curated marketplace and an authenticated SSRF proxy.
MCP operation boundary [#mcp-operation-boundary]
Current tools cover:
* store discovery and available regions;
* product search/ranking and product detail;
* cart creation/view, item add/update/remove, address and shipping selection;
* account login and store-bound session-envelope primitives (the current browser/server handoff mismatch is documented under limitations);
* checkout-readiness validation and merchant-origin checkout link generation.
The built-in completion model receives discovery tools only. Cart capabilities and store-session credentials are attached by direct browser MCP calls for the matching store, not included in model messages. Direct marketplace payment initiation and cart completion are not part of the current tool surface.
Cart capabilities and store sessions [#cart-capabilities-and-store-sessions]
A signed cart capability binds one cart ID to one registered store ID. Existing-cart reads and mutations require the matching capability. Tampered, cross-cart, or cross-store values fail before merchant I/O. The built-in client stores cart IDs and capabilities in browser storage.
The server-side design exchanges store account credentials for a sealed store-session envelope bound to the selected store. Generic incoming `Authorization` and dashboard cookies are not forwarded to merchants. A store credential cannot be reused for another registry entry.
These controls establish narrow possession and routing checks; they do not replace merchant-side cart/customer ownership checks, session expiry, account security, or checkout authorization.
Customer workflow [#customer-workflow]
1. The buyer's prompt is used to query and rank products from curated stores.
2. Product detail and variant choice render through MCP UI.
3. Cart actions call the selected store adapter and preserve a store-bound capability in the browser.
4. Optional account login is intended to produce a credential envelope for that exact store; the current browser handoff must be repaired before relying on it.
5. Address and shipping actions update the merchant cart.
6. Checkout-readiness reports missing cart data.
7. `getCheckoutLink` verifies the capability and creates a fixed merchant-origin handoff URL.
8. Payment, order creation, tax, fraud decisions, inventory commitment, and fulfillment remain the merchant's work.
Operator workflow [#operator-workflow]
The operator reviews registry changes, verifies store ownership and capability support, sets marketplace signing and optional AI secrets, deploys the app, and watches store failures and abuse. Store health, supported operations, freshness, and checkout behavior need separate monitoring; current registry metadata is not a complete capability directory.
Marketplace versus Openship [#marketplace-versus-openship]
Marketplace is the buyer-facing discovery and delegated-cart layer. Openship is an operator-facing order-routing and fulfillment coordinator that links source shops to channels. A merchant may use either or both, but neither should silently take ownership of the other's credentials, state, or responsibility.
# External dashboards
To create a custom Marketplace dashboard, copy the skill and give it to your LLM. The skill will explain Marketplace's non-Keystone architecture before asking what should change, then inspect its conversational UI, MCP tools, adapter registry, cart/session capabilities, and merchant checkout handoff.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Marketplace codebase before proposing work.
# External storefronts
To create a custom Marketplace storefront, copy the skill and give it to your LLM. The skill will explain Marketplace's non-Keystone architecture before asking what should change, then inspect its conversational UI, MCP tools, adapter registry, cart/session capabilities, and merchant checkout handoff.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Marketplace codebase before proposing work.
# Getting started with Marketplace
Requirements and installation [#requirements-and-installation]
Current source requires Node.js 20.9 or later and does not require PostgreSQL.
```bash
npm ci
```
Create an environment file with a signing secret of at least 32 random bytes:
```bash
MARKETPLACE_CAPABILITY_SECRET=replace-with-at-least-32-random-bytes
```
Cart operations require that secret in every AI mode. A separate `MARKETPLACE_SESSION_SECRET` may be used for store-session envelopes; otherwise current source falls back to the capability secret. Use independent managed secrets in a deployed system.
AI configuration [#ai-configuration]
Shared OpenRouter mode uses:
```bash
OPENROUTER_API_KEY=replace-with-an-operator-owned-key
OPENROUTER_MODEL=openai/gpt-4o-mini
OPENROUTER_MAX_TOKENS=4000
```
Without shared values, the built-in UI can accept a buyer's OpenRouter key and store it in browser `localStorage`. That key is sent to the marketplace completion endpoint for requests to the fixed OpenRouter origin. Decide whether browser key storage is acceptable for your audience, threat model, support model, and privacy disclosures before enabling it.
External MCP clients can connect to `/api/mcp-transport/http`. Cart workflows require an MCP UI-capable client and equivalent secure storage/propagation of store-bound capabilities and sessions. Do not expose mutation tools to a client that cannot preserve those boundaries.
Curate stores [#curate-stores]
Edit the server-owned `marketplace.config.json`. Every store needs an explicit stable ID, a root public HTTPS origin, and an adapter that current source implements:
```json
[
{
"storeId": "northwind-goods",
"baseUrl": "https://store.example.com",
"platform": "openfront",
"name": "Northwind Goods"
}
]
```
Current source permits `openfront`. Do not add another platform string until its adapter and conformance/security tests exist. Buyers must select registry IDs; do not restore browser endpoint editing or accept request-supplied origins.
A loopback or private development store is intentionally rejected by production egress rules. Use dedicated public test fixtures or an explicitly isolated test harness rather than weakening the deployed policy.
Run locally and verify [#run-locally-and-verify]
```bash
npm run dev
```
Before deployment, run the current project gates:
```bash
npm test
npm run typecheck
npm run lint
npm run build
```
Use synthetic merchant accounts and carts. Verify product discovery, variant selection, capability creation, tampered/cross-store capability denial, session isolation, address and shipping updates, checkout readiness, exact-origin handoff, redirect refusal, private-network denial, timeout/response limits, merchant failure, and browser storage cleanup.
Deployment [#deployment]
The application can run on a Node.js host that supports Next.js server routes and outbound HTTPS. Deployment needs:
* stable signing secrets and optional shared AI credentials in managed secret storage;
* a reviewed store registry in the deployed artifact;
* DNS and network behavior compatible with address-pinned HTTPS requests;
* outbound network policy, timeouts, request/response limits, rate limits, abuse controls, and observability;
* privacy disclosure for prompts, browser-held keys/capabilities/sessions, connected stores, and AI-provider processing;
* incident response, registry rollback, secret rotation, and store-disable procedures.
The public marketplace at [marketplace.openship.org](https://marketplace.openship.org) was reachable during the 2026-07-20 documentation check. That reachability is only URL evidence; it does not validate every configured store, tool, checkout, privacy, or failure path.
# Openfront Marketplace
[Repository](https://github.com/openshiporg/marketplace) · [Product page](https://openship.org/products/marketplace) · [Marketplace](https://marketplace.openship.org)
Openfront Marketplace is a conversational discovery and cart layer for independently operated stores. It searches a curated store registry, renders product and cart interactions through MCP UI, and hands payment and order completion to the merchant's registered checkout origin.
The marketplace does not have a Keystone, Prisma, or product database. Products, variants, prices, availability, carts, customer accounts, orders, payments, and settlement remain in connected stores. The intended browser state holds store-bound cart capabilities, encrypted store-session envelopes, and optional buyer-supplied OpenRouter configuration. The current encrypted-session browser/server handoff has a documented mismatch, so those primitives are not evidence of a working authenticated cart flow.
Current source implements the Openfront ecommerce adapter. Other platform and vertical adapters are not operating merely because a product name appears in the wider family. Each adapter needs a capability contract, conformance tests, credential isolation, egress controls, and an explicit checkout owner.
Open marketplace and interoperability vision [#open-marketplace-and-interoperability-vision]
A merchant should be able to own its source, storefront, customer relationship, checkout, payment provider, orders, and operating data while participating in many discovery experiences. A marketplace operator curates which stores it trusts; a buyer chooses among those stores; and the merchant remains authoritative for transaction and fulfillment state.
This makes Marketplace distinct from Openfront's database-backed business products and from [Openship](/docs/openship/ecommerce), which coordinates shops, channels, product matches, routed orders, and fulfillment.
Current customer workflow [#current-customer-workflow]
A buyer asks for a product, receives results from configured stores, opens a product view, selects variants, creates or updates a store-owned cart, enters address and shipping details through direct MCP UI actions, and requests a merchant checkout link. The link stays on the exact registered store origin. The marketplace does not collect card or wallet credentials and does not mark the merchant order paid.
Current operator workflow [#current-operator-workflow]
The operator reviews `marketplace.config.json`, assigns stable store IDs, permits only implemented adapters, deploys the registry, configures AI access and signing secrets, and monitors outbound store failures. Buyers cannot submit arbitrary store URLs. Adding a platform means implementing the adapter and security/conformance boundary before extending the registry allowlist.
A reachable website or successful product request is not evidence that every cart, account, checkout, payment, privacy, or failure path is safe. Verify the exact stores, adapters, tools, and deployment you enable.
# Marketplace integrations and boundaries
Platform adapter contract [#platform-adapter-contract]
A platform adapter translates marketplace operations into one registered store's API. The current Openfront adapter implements store/region, product, cart, address/shipping, account-session, and checkout-handoff calls, but the checked-in browser client and MCP server currently disagree on the encrypted session action/header contract. Treat authenticated cart/session use as unavailable until that handoff and its browser contract test are corrected; see [Current limitations](/docs/openfront/marketplace/limitations).
An adapter must not expose raw private GraphQL or generic CRUD merely because the connected store has it. Each method needs a bounded input, customer-safe output, store identity, timeout behavior, error mapping, and tests for missing capability, wrong store, wrong cart, wrong session, redirects, duplicate calls, and partial failure.
Only the Openfront ecommerce adapter is enabled. Restaurant ordering, appointments, hotel stays, rental reservations, gym memberships, pharmacy workflows, and other vertical nouns need their own capability contracts and authority checks. They must not be simulated by renaming product-cart operations.
Store egress boundary [#store-egress-boundary]
The registry is the only source of destinations. The current request path validates root HTTPS origins and public DNS answers, pins the validated address to the socket, preserves TLS hostname validation, refuses redirects, strips generic credentials, and bounds response/time behavior.
Deployment still needs infrastructure-level egress controls, DNS/IPv4/IPv6 test coverage, logs that avoid credentials and customer data, monitoring, rate limits, and an operator kill switch. Application checks are defense in depth, not permission to allow unrestricted network access.
Credential boundary [#credential-boundary]
* Dashboard cookies and generic incoming authorization are not forwarded to stores.
* Store sessions are sealed and bound to one registry store.
* Cart capabilities are signed and bound to one store/cart pair.
* Capabilities and store sessions stay out of completion-model messages in the built-in client.
* Buyer-supplied OpenRouter keys remain a browser/completion-route concern and are not merchant credentials.
Connected stores must independently verify cart/customer ownership, session status, pricing, availability, address, shipping, tax, and checkout authorization.
AI provider boundary [#ai-provider-boundary]
The built-in completion route uses a fixed OpenRouter destination with either operator or buyer configuration. Prompts, tool results, and model output should be treated as data crossing an external provider boundary. Minimize customer and merchant data, disclose processing, cap tokens and tool access, handle provider refusal/outage, and never put store sessions, cart capabilities, passwords, payment credentials, or unrelated store data into model context.
Model recommendations are not merchant facts. Price, availability, variants, shipping, policy, and checkout state must come from the selected store operation at the time they are needed.
Merchant checkout boundary [#merchant-checkout-boundary]
`getCheckoutLink` checks the signed cart capability and builds a fixed path on the registered merchant origin. Payment credentials, fraud checks, tax finalization, order creation, settlement, cancellation, refunds, and fulfillment remain with the merchant.
The marketplace must not claim success because it generated a link or because a merchant page returned HTTP 200. A future embedded settlement design would need a merchant-signed, ownership-checked, idempotent contract with exact amount/currency, replay protection, terminal-state rules, reconciliation, and recovery. Current source intentionally does not expose that flow.
Adding an adapter [#adding-an-adapter]
1. Define capability names using the vertical's actual workflow nouns.
2. Implement the typed adapter against a dedicated synthetic fixture.
3. Add customer-safe projections and reject private/raw fields.
4. Prove registry-only destinations, credential isolation, capability and session binding, and redirect/private-network denial.
5. Test idempotency, concurrency, timeout, duplicate request, stale data, partial response, provider error, and handoff behavior.
6. Add health/capability metadata and an operator disable path.
7. Extend the code-owned allowlist only after review.
# Marketplace current limitations
Adapter coverage [#adapter-coverage]
Current source supports curated Openfront ecommerce stores. Shopify, WooCommerce, BigCommerce, Restaurant, Hotel, Rental, Pharmacy, and other platform or vertical names are not working Marketplace adapters without separate source and conformance evidence.
There is no public multi-vertical capability protocol or complete adapter conformance suite yet. Store health, capability freshness, version compatibility, and operator disable metadata also need a stronger contract.
Commerce ownership [#commerce-ownership]
The marketplace does not own a product database, inventory, merchant customer account, order, payment, refund, or fulfillment record. It can prepare a merchant cart and generate an exact-origin handoff. It cannot prove payment or order completion, and it has no general reconciliation feed back from merchants.
This is a deliberate interoperability boundary, not a promise that every merchant flow works. Connected stores remain responsible for identity, cart ownership, price, inventory, tax, shipping, payment, fraud, order, cancellation, refund, customer service, legal terms, and fulfillment.
Browser and privacy boundary [#browser-and-privacy-boundary]
The intended client boundary stores cart IDs, signed capabilities, encrypted store-session envelopes, and optional buyer OpenRouter settings in browser storage. Browser compromise, shared devices, extension access, XSS, stale sessions, and storage persistence therefore matter. Current source does not provide a centralized account that synchronizes or remotely revokes all browser-held state.
The checked-in browser handoff is currently inconsistent: cart tools emit a `saveStoreSession` action with `sessionEnvelope`, while the browser MCP client consumes `saveSessionToken`/`sessionToken` and sends raw session headers that the server rejects in favor of `x-store-session-envelope`. The encryption/capability primitives exist, but do not describe authenticated browser cart/session handoff as functioning until the client/server action contract and focused browser test are corrected.
A deployment needs a clear retention and cleanup policy, content-security policy, dependency review, XSS testing for store/model content rendered through MCP UI, session and capability expiry/rotation, privacy disclosure, and a decision about whether buyer-supplied key storage is permitted.
AI limitations [#ai-limitations]
Model output can be incomplete, biased, unsafe, or wrong. Ranking logic and prompts do not guarantee neutral discovery, complete catalog coverage, current merchant facts, or accessibility. The built-in model receives discovery tools only, but external MCP clients may have different behavior and must preserve equivalent credential and capability separation.
Do not treat a model statement as a price, availability, policy, medical/legal recommendation, merchant commitment, or completed transaction. Read authoritative facts from the connected store and hand consequential work to the proper owner.
Security and reliability work [#security-and-reliability-work]
Current source includes registry validation, public-network egress checks, DNS pinning, redirect refusal, credential-isolation primitives, store-bound envelope code, signed cart capabilities, request-local MCP handling, and bounded merchant requests. A deployed service still needs infrastructure egress policy, rate/body/concurrency limits, bot and abuse controls, logs/metrics/alerts, store health, retries that do not duplicate mutations, incident response, secret rotation, disaster recovery, and an operator store-disable path.
Full browser end-to-end coverage against dedicated disposable merchant fixtures remains incomplete. Verify all enabled tools and stores under success, timeout, malformed response, stale cart, wrong account, wrong store, cross-cart, redirect, DNS/IP, rate limit, merchant outage, AI outage, and checkout-return conditions.
A repository, product page, or hosted Marketplace response establishes URL availability only. It does not establish adapter coverage, store authenticity, transaction success, privacy compliance, security certification, uptime, or merchant settlement.
# External dashboards
To create a custom Real Estate dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Real Estate's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Real Estate codebase before proposing work.
# External storefronts
To create a custom Real Estate storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Real Estate's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Real Estate codebase before proposing work.
# Openfront Real Estate
[Source](https://github.com/openshiporg/openfront-realestate) · [Catalog](https://openship.org/products/openfront-real-estate)
Openfront Real Estate combines a public listing site with brokerage work for leads, showings, offers, deals, documents, inspections, title/escrow milestones, commissions and reports.
Escrow, mortgage, title and commission models record workflow facts. They do not prove fund custody, lending, title services, settlement, MLS authorization or regulatory compliance.
Architecture and schema [#architecture-and-schema]
`Brokerage` and active `BrokerageMembership` are the tenant roots; agent assignment narrows selected records. Access helpers and lifecycle hooks validate brokerage relationships rather than treating a listing feed or team as a tenant. The graph includes brokerage settings, properties/media/neighborhoods, listings/syndication, leads/activities/routing, saved properties/searches/alerts, showings/feedback/open houses, market comps/valuations, offers/deals, disclosures/inspections/documents, transaction milestones, title orders, escrow and commission records, mortgage applications, source feeds, agent teams/tasks and users.
The public site has listing index/detail plus inquiry and showing-request flows. Operator routes under `/dashboard/platform` cover listings, leads, pipeline, showings, operations, and reports.
Main workflow [#main-workflow]
A brokerage publishes a listing. A visitor or agent creates a lead, saved search, showing request or offer. Operators qualify the lead, schedule the showing, move the offer/deal through controlled states, and track inspection, disclosure, title and closing milestones. External settlement remains outside the current payment boundary.
Bounded GraphQL operations [#bounded-graphql-operations]
Public projections expose brokerage identity and published listings; customer operations submit inquiries and showing requests, save properties/searches, and manage alerts. `platformProjections` and `platformCommands` provide task-oriented operator reads/actions. Commands record lead activity, routing and dedupe; schedule and complete showings; create/counter/transition offers; move deals and milestones; manage disclosures, documents and commissions; and queue/replay source and outbox work. Mortgage, title, escrow, and commission commands preserve local workflow facts but do not execute regulated settlement.
Setup and onboarding [#setup-and-onboarding]
Use a disposable PostgreSQL database. `runRealEstateOnboarding` creates a synthetic brokerage, membership, settings, listings, leads, and pipeline data for local evaluation. Run it twice on an isolated current database, then test public listing projections, lead ownership and routing, showing conflicts, lifecycle snapshots, and deal transitions. Seeded brokerage data is not a hosted demo.
Integrations [#integrations]
`SourceFeed` and ListingSyndication describe local inbound/outbound state, but current source has no integration adapter registry. There is no evidenced MLS/IDX, title, escrow, mortgage, calendar, messaging, signing, or payment adapter. Credentials must be unreadable, and a feed is not working until fetch/auth, mapping, dedupe, retries, provenance, and reconciliation pass. Payment processing is currently N/A.
Security and deployment [#security-and-deployment]
Protect source credentials, lead contact data, mortgage applications, disclosures, and documents. Keep accepted/closed snapshots immutable. Railway builds then migrates at start. The checked-in lint script uses the removed `next lint` command under Next.js 16, and the README references a missing `env.example`; neither is a valid release instruction without repair.
Deploy only after reviewed migrations, current schema/type/tests/build, cross-brokerage/agent negatives, public-intake abuse controls, showing conflicts, lifecycle replay/terminal denial, source-feed failures, restore, and responsive transaction workflow checks. Current source does not custody escrow, originate mortgages, provide title/settlement, execute commission payments, connect an MLS, or establish brokerage/regulatory certification.
# Pharmacy architecture and workflows
Application surfaces [#application-surfaces]
The public application provides the pharmacy storefront at `/`, cart at `/cart`, and checkout at `/checkout`. Public queries return enabled locations and non-prescription catalog products. A guest cart uses an opaque token; the stored form is hashed. A successful order returns a separate token accepted by the bounded `guestPharmacyOrder` projection. Current source has no public order-status route or storefront helper that consumes that projection, so it is an API boundary rather than a complete customer order-tracking surface.
Authenticated operators use `/dashboard/platform/*` routes:
| Area | Current routes and work |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prescriptions | Queue, intake, and prescription task pages for source verification, pharmacist review, dispensing authorization, lot selection, and dispensing evidence |
| Inventory | Lot/expiry/cold-chain queue, controlled receipt, lot task, quantity adjustment, temperature-excursion evidence, and movement history |
| Recalls | Recall queue and recall task for opening a bounded recall and quarantining affected lots |
| Orders | Order queue and task pages for immutable lines, payment state, and controlled fulfillment transitions |
| Patients and POS | Patient-safe directory/detail and a location-scoped non-prescription POS catalog |
| Integrations and evidence | Provider state, audit timeline, outbox timeline, failed-delivery queue, and permissioned replay |
Generic Keystone administration still exists for permitted configuration records. Consequential lifecycle and evidence records omit raw create/update/delete GraphQL fields; focused platform pages call named operations instead.
Conceptual data model [#conceptual-data-model]
* **Business and staff:** organizations, locations, memberships, users, and roles establish tenant and location authority.
* **Medication and catalog:** drug references carry bounded medication identity such as NDC snapshots; products connect that reference to pharmacy-owned SKU, availability, price, tax, prescription requirement, and cold-chain policy.
* **Patients and prescribers:** patient profiles, prescribers, consent records, prescriptions, items, reviews, refills, and transfers keep identity, authorization, consent, and medication facts explicit.
* **Inventory and safety evidence:** suppliers, lots, immutable movements, recalls, temperature excursions, and human approvals connect what was received, held, quarantined, released, recalled, or dispensed.
* **Commerce and fulfillment:** carts and items lead to immutable order lines, payment sessions/payments, fulfillment, and guest-safe order access. Prescription requirement, NDC, name, quantity, and price are snapshotted where needed.
* **Operations evidence:** audit and outbox events identify the actor, aggregate, event key, and bounded metadata without using raw patient, prescription, payment, or provider payloads as the audit record.
Model presence is not a capability claim. Capability comes from a routed, permissioned operation with a defined lifecycle, transaction, projection, test, and operational owner.
Bounded GraphQL queries [#bounded-graphql-queries]
Customer queries are limited to public locations/catalog, token-scoped cart, enabled payment choices, and token-scoped guest order. Operator projections cover the catalog queue, POS catalog, setup options, prescription/inventory/recall/order queues and tasks, patient-safe views, provider state, summary counts, onboarding state, audit timeline, and outbox state.
Those projections deliberately omit patient contact and birth details from patient-safe views; provider credentials and adapter function names; payment payload data; guest-token hashes; audit metadata; and outbox payload/error text. Requesting an omitted field fails at the schema boundary.
Named mutations [#named-mutations]
Current source registers these pharmacy operations:
* **Customer commerce:** `createPharmacyCart`, `addPharmacyCartItem`, `updatePharmacyCartItem`, `removePharmacyCartItem`, `initiatePharmacyPaymentSession`, and `submitPharmacyOrder`.
* **Prescription and patient requests:** `intakePharmacyPrescription`, `recordPrescriptionReview`, `authorizePrescriptionDispensing`, `recordPrescriptionDispensing`, `requestPrescriptionRefill`, `requestPrescriptionTransfer`, and `approvePrescriptionTransfer`.
* **Inventory, recall, and cold chain:** `receivePharmacyInventoryLot`, `adjustPharmacyInventoryLot`, `openPharmacyRecall`, `quarantineRecallLots`, and `recordTemperatureExcursion`.
* **Operations:** `transitionOrderFulfillment`, `replayPharmacyOutboxEvent`, `runPharmacyOnboarding`, and `setPharmacyOnboardingStatus`.
These are application operations, not declarations that an external pharmacy network, insurer, wholesaler, tax service, sensor, delivery provider, or regulator accepted the result.
Prescription and dispensing path [#prescription-and-dispensing-path]
The controlled path is intake -> source verification -> pharmacist review -> approval or rejection -> dispensing authorization -> eligible lot selection -> dispensing evidence. A prescription cannot jump from draft to dispensed. Terminal records do not re-enter active workflow. Dispensing verifies the authorized prescription, location, product/lot match, expiry, available quantity, quarantine quantity, recall/cold-chain eligibility, pharmacist permission, attestation, and reason.
Software checks do not make a prescription valid or dispensing lawful. The authorized pharmacist remains responsible for clinical and legal review, identity, prescriber authority, substitution, counseling, controlled-substance rules, jurisdictional records, and the physical act of dispensing.
Lot, recall, and cold-chain path [#lot-recall-and-cold-chain-path]
Receipt creates lot identity, quantity, approval, movement, audit, and outbox evidence together. A cold-chain lot without complete evidence starts quarantined. A temperature excursion records the observed range and time, quarantines the lot, marks evidence incomplete, and creates disposition evidence. Opening a recall binds a recall identity and selected tenant-owned lots; quarantining the recall moves each affected lot to a recalled/quarantined state and records immutable movements.
The implementation records evidence supplied by authorized people or adapters. It does not ingest calibrated sensor data, validate a shipping lane, identify every affected patient, submit regulatory reports, or prove that a physical product was removed from circulation.
Commerce and fulfillment path [#commerce-and-fulfillment-path]
The public cart rejects any product whose prescription requirement is not `none`. Checkout re-reads server-owned product, lot, location, payment, amount, and availability facts; creates immutable order and line snapshots; decrements eligible inventory in a transaction; and records audit/outbox evidence. Current concurrency evidence used database locks so only one competing one-unit checkout committed, but each deployed database and workflow still needs load and failure testing.
Order fulfillment is separate from prescription review and dispensing. A paid OTC order can move through fulfillment states; a medication order cannot use a generic fulfillment transition to bypass pharmacist-controlled dispensing.
# External dashboards
To create a custom Pharmacy dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Pharmacy's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Pharmacy codebase before proposing work.
# External storefronts
To create a custom Pharmacy storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Pharmacy's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Pharmacy codebase before proposing work.
# Getting started with Pharmacy
Requirements [#requirements]
Current `package.json` requires Node.js 20 or later. The database-backed application uses PostgreSQL, Prisma, Keystone, Next.js, and a long random session secret.
Install the locked dependency tree from the repository root:
```bash
npm ci
```
Create a local environment file for a database you are authorized to change:
```bash
DATABASE_URL=postgresql://user:password@127.0.0.1:5432/openfront_pharmacy
SESSION_SECRET=replace-with-a-long-random-secret
PUBLIC_SIGNUPS_ALLOWED=false
```
Optional integrations have additional variables documented in [Integrations and boundaries](/docs/openfront/pharmacy/integrations-boundaries). Keep provider secrets outside source control.
Review and apply migrations [#review-and-apply-migrations]
The current working source contains eleven migration directories: seven inherited starter migrations plus Pharmacy foundation, behavioral-depth, tenant-identity, and required-relationship changes. Review the complete history and test both an empty database and the actual upgrade path before applying it to any retained data.
```bash
npm run keystone:build
npm run migrate
```
Both `npm run dev` and `npm run build` invoke `npm run migrate`. They can change the database referenced by `DATABASE_URL`. Do not point either command at an uncontrolled, shared, or production database. Separate migration deployment from application build before adopting an immutable-image release process.
Start the local application only after migration review:
```bash
npm run dev
```
The storefront is at `/`, the operator dashboard at `/dashboard`, and GraphQL at `/api/graphql`. Do not use schema push or a destructive reset as a substitute for reviewed migrations.
Create the first operator [#create-the-first-operator]
The shared dashboard initialization flow creates the first authenticated user. Keep public signup disabled unless a separate enrollment and identity process is designed. Grant only the organization, location, catalog, inventory, recall, patient, prescription, dispensing, order, payment, integration, onboarding, audit, and consequential-approval permissions required by the person's actual job.
A role with dashboard access is not automatically a pharmacist. Pharmacist credential and scope verification happen outside this repository and must be enforced by the deploying organization.
Pharmacy onboarding [#pharmacy-onboarding]
`runPharmacyOnboarding` requires the onboarding permission and creates synthetic evaluation data for an organization, location, drug references, products, lots, a patient/prescriber/prescription path, provider placeholders, and onboarding state. It can create a development-only demo payment provider only when both conditions are true:
```bash
NODE_ENV=development
ALLOW_DEMO_PAYMENTS=true
```
`setPharmacyOnboardingStatus` only requires a signed-in user and changes that user's status; it does not authorize seed execution or pharmacist work. The seeded eRx, wholesaler, insurance, and tax provider records are explicitly unconfigured. Seeded prescriptions, patients, drug references, lots, prices, and provider records are synthetic examples, not validated operating data. Re-run onboarding on an isolated database to check idempotency, then remove or clearly segregate evaluation records before any real deployment.
Source checks [#source-checks]
Current source defines these checks:
```bash
npm test
npm run typecheck
npm run keystone:build
npm run build
```
The repository's `lint` script currently delegates to `next lint`; verify that command against the installed Next.js version before treating lint as a release gate. A passing build, test, browser render, or HTTP response proves only the behavior exercised by that check. It does not certify licensure, legal compliance, clinical safety, patient privacy, provider acceptance, physical inventory, or lawful dispensing.
Deployment sequence [#deployment-sequence]
1. Define the licensed organization, authorized locations, accountable pharmacist roles, policies, jurisdictions, providers, retention rules, and incident owners.
2. Review the dependency tree and resolve security findings under an owner-approved framework baseline.
3. Back up the target database and test restoration.
4. Review and deploy migrations as a separate controlled step.
5. Configure stable session, SMTP, payment, storage, and other secrets through a managed secret system.
6. Configure only adapters that have contract, credential, webhook, retry, reconciliation, and failure tests.
7. Run cross-organization, cross-location, patient-privacy, permission, lifecycle, concurrency, idempotency, duplicate-callback, and rollback tests against the exact release snapshot.
8. Exercise prescription, dispensing, lot, recall, cold-chain, OTC checkout, payment, fulfillment, audit, and recovery workflows with synthetic data and accountable human reviewers.
9. Stage rollout with monitoring, backups, incident response, provider escalation, rollback, and downtime procedures.
Use [Current limitations](/docs/openfront/pharmacy/limitations) as a minimum pre-deployment review, not a complete jurisdiction-specific checklist.
# Openfront Pharmacy
Openfront Pharmacy is a self-hosted pharmacy commerce and operations application. It combines a public over-the-counter catalog and checkout with authenticated prescription intake and review, pharmacist-authorized dispensing, patient-safe views, lot and expiry inventory, recalls, temperature-excursion evidence, order fulfillment, audit history, and integration boundaries.
Openfront Pharmacy does not grant pharmacy licensure, pharmacist authority, prescribing authority, dispensing authority, or regulatory approval. It is not documented as HIPAA compliant, a certified e-prescribing system, an insurance-adjudication system, a controlled-substance system, a validated cold-chain monitoring system, or a certified recall system. Each deployment needs independent legal, regulatory, clinical-safety, privacy, security, provider, and operational validation.
Human authority is the control boundary [#human-authority-is-the-control-boundary]
Prescription acceptance, review, dispensing authorization, physical dispensing, prescription transfer approval, inventory receipt or adjustment, recall disposition, temperature-excursion disposition, and consequential fulfillment transitions are human-controlled work. Current operations require an authenticated role with the relevant location scope; high-consequence mutations also require a reason and explicit attestation and record bounded `HumanApproval`, audit, inventory-movement, or outbox evidence.
AI and MCP features do not replace a pharmacist, prescriber, technician operating under authorized supervision, recall coordinator, privacy officer, or other accountable professional. An agent may help find information or prepare a request only within a separately approved scope. It must not diagnose, prescribe, approve, substitute, dispense, override a hold, release recalled or cold-chain-affected inventory, or claim that an external provider accepted an action.
Product surfaces [#product-surfaces]
* The public storefront exposes pharmacy locations and non-prescription products, a guest-safe cart, payment-session initiation, and checkout. GraphQL has a token-scoped guest-order projection, but current source has no public order-status route or client flow for it.
* Pharmacy operators have focused queues and task pages for prescriptions, inventory lots, recalls, orders, patients, POS catalog, integration state, audit history, and outbox delivery state.
* Prescription products cannot enter the public cart. They follow the authorized prescription path.
* Private records are not exposed to customer screens as raw Keystone lists. Customer and operator pages use pharmacy-specific GraphQL projections and named operations.
Ownership and tenancy [#ownership-and-tenancy]
`Organization` is the business boundary; `PharmacyLocation` is the operating scope for inventory, prescriptions, fulfillment, and many staff permissions. Active organization and location memberships constrain operator reads. Patient access is separately bounded to the linked user or an authorized organization member. Sensitive customer-facing results use explicit projections that omit hashes, contact details, raw consent evidence, provider configuration, audit metadata, and outbox payloads.
The application uses Next.js, Keystone, GraphQL, Prisma, and PostgreSQL. The Pharmacy pages document its medication, evidence, and human-authority boundaries.
# Pharmacy integrations and boundaries
An integration record describes a provider choice and validation state. It is not proof that credentials work, a contract exists, a request was transmitted, the provider accepted it, money moved, a claim adjudicated, a product shipped, or a regulator received a report.
Adapter contract [#adapter-contract]
Current external adapter types distinguish eRx, wholesaler, insurance, and tax providers. Requests carry a provider code, an operation (`submit`, `status`, or `webhook`), a bounded payload, and optional idempotency key. Results can report accepted, pending, or rejected with an external reference and bounded data.
The registries are code-owned; a database value cannot select an arbitrary module. At present, each eRx, wholesaler, insurance, and tax registry contains only an `unconfigured` adapter that fails closed. Onboarding creates corresponding provider placeholders with unconfigured status.
Before adding a provider:
* define a provider-specific typed request and response rather than forwarding a raw internal record;
* bind credentials to one organization, provider, operation, and destination;
* keep secrets unreadable through GraphQL and out of logs, audit metadata, and outbox payloads;
* authenticate inbound traffic from the raw body, claim event IDs, reject stale/replayed callbacks, and make retries idempotent;
* map pending, accepted, rejected, partial, reversed, and unavailable states without inventing success;
* add reconciliation, operator exceptions, timeouts, rate handling, redaction, retention, and provider escalation.
Prescription and eRx [#prescription-and-erx]
Current source stores prescriber identity, verification state, prescription source, external reference, source-verification state, and pharmacist evidence. It does not include a certified eRx network connection or prove that a prescriber is licensed, has authority for the medication and jurisdiction, signed the prescription, or transmitted it through an accepted network.
Do not auto-approve a prescription because an adapter returned data. The local pharmacist review and dispensing authority remain required. Electronic controlled-substance prescribing needs separate identity proofing, signing, audit, access, retention, and provider certification that this source does not supply.
Wholesalers and supply chain [#wholesalers-and-supply-chain]
Supplier, product, drug-reference, lot, serial, receipt, expiry, cold-chain, movement, and recall relationships create a local evidence graph. The wholesaler registry is unconfigured. There is no current purchase-order transmission, pedigree/trace exchange, inbound ASN, supplier credential check, shortage feed, or automated recall feed.
A wholesaler adapter must preserve organization/location ownership, product and lot identity, quantities, dates, serialized evidence where applicable, duplicate shipment handling, quarantine defaults, and receiving exceptions. Local records do not establish DSCSA or other supply-chain compliance.
Insurance and patient payment [#insurance-and-patient-payment]
The insurance registry is unconfigured. Current source does not perform eligibility, benefit, formulary, prior authorization, claim submission, reversal, coordination of benefits, remittance, or patient-cost adjudication.
Do not use a configured record or local price as an insurance result. An insurance adapter needs patient and prescriber privacy controls, purpose limitation, exact transaction types, payer response provenance, reversals, retries, reconciliation, retention, and an operator path for rejects and ambiguous responses.
Tax [#tax]
The tax registry is unconfigured. Checkout has tax fields, but no current external tax service establishes product taxability, jurisdiction, exemption, filing, or remittance. Server-owned tax calculation and immutable order snapshots must precede payment; jurisdictional tax review remains external.
Payments [#payments]
The code-owned payment registry includes Stripe and a local demo adapter. The demo adapter is disabled in production, requires explicit development enablement, marks its results `demoOnly`, and rejects webhooks. Stripe source can create, capture, refund, and inspect payment intents and contains signature-verification code that expects `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET`.
Current application routes do not expose a dedicated Stripe webhook endpoint. Do not describe asynchronous Stripe settlement, refund callbacks, or reconciliation as operating until a bounded route, raw-body verification, replay protection, event ownership, lifecycle mapping, and recovery flow are implemented and tested. Never accept a browser-supplied paid state or amount.
Messaging, storage, and delivery [#messaging-storage-and-delivery]
SMTP variables and mail utility source exist, but message delivery is not a pharmacy notification service. A deployed system needs consent, channel preference, minimum-necessary content, bounce/failure handling, retry, quiet hours, retention, and an operator record. Do not put medication or patient detail into email/SMS without an approved privacy and security design.
The starter includes S3-related environment support, but current pharmacy workflows do not establish a validated prescription/document vault. Delivery-window and fulfillment records do not establish a courier integration, identity check, chain of custody, temperature-controlled delivery, counseling, or jurisdictional delivery authority.
Outbox and workers [#outbox-and-workers]
Domain mutations write bounded outbox events in the same transaction as local state. Operator queries show timeline and failed-delivery state, and a permissioned replay mutation can return failed work to pending. Adapter acknowledgement is intentionally internal rather than exposed through public GraphQL.
An outbox row is not delivery. A production deployment still needs a worker with event claiming, lease/timeout behavior, destination-specific idempotency, redaction, retry/backoff, dead-letter review, metrics, alerting, and reconciliation.
AI and MCP [#ai-and-mcp]
Completion and MCP transport are application-assistance surfaces, not pharmacy authority. Browser-configured AI keys and prompts must not receive patient, prescription, payment, credential, or raw provider data. Any agent operation needs explicit allowlisted tools, least privilege, organization/location scope, human review for consequential work, attributable evidence, and a fail-closed response when authority or provider state is uncertain.
No adapter, test double, provider row, successful HTTP response, or AI output can certify a pharmacy workflow. Provider contracting, external validation, licensed-human authority, and deployment controls remain separate requirements.
# Pharmacy current limitations
This page is a source boundary, not legal advice or a complete pharmacy-readiness checklist. Requirements depend on the pharmacy's activities, products, facilities, providers, patients, staff, and jurisdictions.
No license or certification is supplied [#no-license-or-certification-is-supplied]
The repository does not license a pharmacy or its locations; credential pharmacists, technicians, prescribers, or couriers; authorize prescribing or dispensing; approve controlled-substance activity; certify HIPAA or other privacy/security compliance; certify eRx, EPCS, insurance, tax, payment, wholesaler, recall, cold-chain, counseling, delivery, accessibility, or consumer-protection behavior; or establish that hosting and operations meet an applicable rule.
Tests, migrations, type checks, builds, browser screenshots, and HTTP responses are engineering evidence for the paths exercised. They are not regulatory, clinical, legal, security, payment, or operational certification.
Pharmacist and accountable-human work remains external [#pharmacist-and-accountable-human-work-remains-external]
The software records role checks, attestation, reason, and bounded approval evidence. It cannot determine whether the actor is currently licensed, working within scope, free of conflicts, following pharmacy policy, or making a clinically appropriate decision. It does not replace prospective drug-use review, interaction/allergy/duplicate-therapy checks, dose and indication review, substitution law, counseling, prescriber communication, identity checks, or physical verification.
Consequential agent action is not an alternative. Agents must not approve, prescribe, substitute, dispense, release held inventory, close a recall, adjudicate a claim, or report an external effect without an independently authorized workflow and accountable human decision.
Prescription and controlled-substance gaps [#prescription-and-controlled-substance-gaps]
The local prescription lifecycle supports intake, source-verification state, pharmacist review, dispensing authorization, lot selection, refill/transfer requests, and evidence. It does not provide a certified prescription network, authoritative prescriber/DEA verification, EPCS signing, jurisdiction-specific transfer/refill rules, PDMP integration, controlled-substance inventory and perpetual-log controls, partial-fill rules, suspicious-order monitoring, mandated forms, or regulator reporting.
Until those requirements are designed and independently validated, do not use current source for controlled substances or treat an external reference as proof of a valid prescription.
Patient privacy and clinical-safety gaps [#patient-privacy-and-clinical-safety-gaps]
Bounded projections and tenant filters reduce exposure, but they do not establish a complete privacy/security program. Current source does not by itself supply identity proofing, workforce provisioning/deprovisioning, managed encryption and key rotation, device/session policy, centralized audit monitoring, breach response, business-associate controls, data residency, legal hold, patient access/amendment/export, deletion/retention jobs, backup restoration, disaster recovery, or penetration testing.
Do not place real patient, prescription, insurance, payment, or clinical data into a deployment until the full data flow, minimum-necessary access, field encryption/tokenization, logs, backups, integrations, support access, and incident procedures are reviewed.
Inventory, recall, and cold-chain gaps [#inventory-recall-and-cold-chain-gaps]
Lot, expiry, movement, quarantine, recall, and temperature-excursion records preserve useful local evidence. They do not prove physical count, product authenticity, pedigree, calibrated sensor accuracy, storage-lane qualification, excursion stability, recall completeness, patient notification, supplier/regulator acknowledgement, destruction, or removal of physical stock.
A production workflow needs device/provider validation, chain of custody, cycle counts, exception ownership, reconciliation, recall-source ingestion, affected-order/patient tracing, communications, disposal evidence, reporting, and jurisdiction-specific retention.
Provider execution gaps [#provider-execution-gaps]
* eRx, wholesaler, insurance, and tax registries fail closed with unconfigured adapters.
* Stripe adapter functions exist, but the current app has no dedicated Stripe webhook route and no complete asynchronous settlement/reconciliation operation. The storefront initiates a PaymentIntent and immediately requires an authorized/captured state without rendering Stripe confirmation UI, so production Stripe checkout cannot complete through the current browser flow.
* The local payment adapter is development-only and does not represent money movement.
* `guestPharmacyOrder` provides a token-scoped API projection, but there is no public order-status route or storefront retrieval flow.
* Outbox events have no complete production worker/delivery service in current source; replay only changes local bounded state.
* SMTP and storage support do not establish pharmacy-approved messaging or document handling.
* Fulfillment records do not establish courier identity, counseling, controlled delivery, cold-chain delivery, or proof of handoff.
Runtime and dependency gaps [#runtime-and-dependency-gaps]
Development and build commands currently deploy migrations against `DATABASE_URL`; release engineering should separate migration and build authority. The migration history includes inherited starter migrations and must be tested against both empty and retained databases. The current evidence records unresolved dependency audit findings under the inherited framework baseline; review and remediate or formally mitigate the exact release dependency tree before deployment.
Operational readiness also requires resource limits, rate and body limits, abuse controls, observability, alerting, job supervision, database pooling, backup/restore drills, key management, provider outage behavior, incident response, staged rollout, rollback, support ownership, and documented downtime procedures.
Tenancy and concurrency require release-specific proof [#tenancy-and-concurrency-require-release-specific-proof]
Current source applies organization/location membership filters and named operations recheck scope. Transaction and lock evidence covers selected one-unit checkout and workflow cases. Every release still needs cross-organization, cross-location, patient-owner, wrong-role, relationship-move, guessed-token, simultaneous dispense, simultaneous checkout, duplicate callback, retry, rollback, and partial-provider-failure tests against the actual PostgreSQL and deployment configuration.
What to validate before real use [#what-to-validate-before-real-use]
At minimum, obtain qualified pharmacy, legal, privacy, security, clinical-safety, accessibility, payment, provider, infrastructure, and jurisdictional review; define accountable operators; configure only contracted and tested providers; run the complete synthetic workflow and negative suite; verify physical procedures against system states; test recovery; and document which activities remain prohibited.
See [Getting started](/docs/openfront/pharmacy/getting-started) for deployment sequencing and [Integrations and boundaries](/docs/openfront/pharmacy/integrations-boundaries) for provider contracts.
# External dashboards
To create a custom Rental dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Rental's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Rental codebase before proposing work.
# External storefronts
To create a custom Rental storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Rental's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Rental codebase before proposing work.
# Openfront Rental
[Source](https://github.com/openshiporg/openfront-rental) · [Catalog](https://openship.org/products/openfront-rental)
Openfront Rental is a professional short-term accommodation and host-property-management product. Current models and routes are accommodation-specific: properties and listings, units/bedrooms/beds, guest search and quote checkout, reservations and trip links, stay agreements, host calendar/pricing/inbox/tasks, channels, payments, payouts, reviews, and owner statements.
Historical equipment-rental names remain in old migration/audit history, but they are not the current active model or route graph. Current limitations are live-runtime concerns: no OTA adapter, no customer card processor, unproved booking concurrency, manual settlement, and deployment-specific host/guest/token verification.
Guest and host surfaces [#guest-and-host-surfaces]
Guest routes are `/search`, `/stays/[slug]`, `/checkout/[quoteToken]`, `/trips/[token]`, and `/agreements/[token]`. A guest searches published listings, requests a server-owned quote, creates a reservation, follows token-bound trip information, signs a stay agreement, and can request cancellation through the bounded lifecycle.
Host/operator routes under `/dashboard/platform` cover properties, listings, calendar, pricing, reservations, guests, inbox, channels, payments, payouts, reviews, agreements, and property tasks. These are professional host operations, not equipment inventory or ecommerce product pages.
Schema and tenancy [#schema-and-tenancy]
`HostOrganization` and active `HostMembership` are the tenant roots. Property, listing, accommodation unit, guest, quote, reservation/line, agreement, payment, payout, channel, task, review, owner, and conversation records carry host organization scope. Access helpers validate active membership, prevent host moves, and verify that submitted related-record IDs belong to the same host—not only that the previous row was visible.
The accommodation graph includes properties/addresses, units, bedrooms/beds, amenities, listings/media, rules, rates/seasonal rates/fees/taxes, quotes, reservations and immutable line snapshots, guest parties/profiles, agreements, conversations/messages, calendar blocks, channel listings/reservations/sync events, property tasks, reviews, deposits/damage claims, payments/refunds/payouts/providers/webhook events, owners/assignments/statements, and marketplace settings.
Host scope is a source boundary, not runtime proof. Test creation, relationship changes, organization switching, inactive membership, and cross-host IDs across every generated and custom path.
Bounded queries and commands [#bounded-queries-and-commands]
Public projections are `searchStays`, `publicMarketplaceSettings`, `publicListing`, `publicStayQuote`, `publicTrip`, and `publicStayAgreement`. They expose published/owned facts through exact slugs or opaque tokens instead of raw host records.
Named mutations include `quoteStay`, `createStayReservation`, `requestStayCancellation`, `signStayAgreement`, `transitionStayReservation`, `initiateStayPayment`, `captureStayPayment`, and onboarding status. Reservation and quote snapshots preserve accepted dates, rates, fees, taxes, guest counts, currency, and listing facts. Host calendar actions remain scoped model/action operations rather than one broad workspace projection.
Generated CRUD must not bypass host ownership, quote expiry, reservation snapshots, availability locks, agreement tokens, operator settlement, or payment webhook state. Prove simultaneous quote/reservation behavior and idempotent replay against the deployed database before accepting bookings.
Onboarding and synthetic runtime [#onboarding-and-synthetic-runtime]
Current `seed.json` creates Commonstay Collective accommodation data: one host organization and membership, properties/listings, units/bedrooms/beds, amenities, guests, reservations, calendar blocks, tasks, channel mappings, messages, reviews, payouts, owner statements, and manual payment evidence.
`seedAccommodationPlatform.ts` is restricted to the named `runtime_rental` local database and writes through a serializable transaction with reservation snapshot/line creation. `runAccommodationOnboardingWorkflow` controls status and cleanup. Run the flow twice on an isolated database and inspect stable host ownership, listing/quote/reservation relations, token access, calendar blocks, agreement state, and seed cleanup. It is not a hosted accommodation service or provider connection.
Payments and channels [#payments-and-channels]
The enabled seed provider is manual `pp_system_default`, intended for operator-recorded settlement. A signed-test provider exists to exercise webhook authentication; it is not a real customer processor. Current source does not establish Stripe, PayPal, bank transfer, deposit custody, payout rails, chargebacks, or customer card checkout.
Channel connection/listing/reservation/sync models are local integration state. No Airbnb, Vrbo, Booking.com, Expedia, or other OTA adapter is implemented by current source. A configured channel or synthetic sync row does not prove authentication, availability exchange, reservation import, cancellation, retries, or reconciliation.
Any provider addition needs code-owned adapter selection, unreadable credentials, server-derived amounts, signed raw-body callbacks, replay IDs, durable attempts, cancellation/refund mapping, and operator reconciliation. Calendar/channel updates must not override accepted reservation snapshots or create double bookings.
Deployment and current limitations [#deployment-and-current-limitations]
The repository uses Node.js/PostgreSQL and Railway's build-then-migrate-at-start shape. The checked-in lint script invokes `next lint` under Next.js 16 and is not a valid gate until the owning source migrates it to the ESLint CLI. Do not report lint as passing based on that script.
Before real guest data, run current migrations, schema/type/tests/build, cross-host negatives, token expiry/revocation, simultaneous booking, manual-settlement authorization, signed-test webhook replay, cancellation, channel outage, payout/statement arithmetic, backup restoration, responsive browser, monitoring, and incident checks against the exact source/deployment.
Current source does not provide equipment-rental behavior, customer card processing, OTA synchronization, payout execution, deposit custody, identity verification, property licensing, tax remittance, guest screening, insurance, physical access, or operational certification. The host graph is stronger than the stale cutover warning it replaces, but payment, provider, concurrency, security, privacy, accessibility, and jurisdiction-specific obligations still require independent implementation and verification.
# AI assistant
The AI assistant in Openfront Restaurant is not a canned FAQ bot. It sits on top of the same GraphQL API the rest of the product uses, and it discovers the schema through MCP tools before it makes changes.
That matters for one simple reason: when the assistant updates a menu item or looks up an order, it is doing real work against real data.
What it is good at right now [#what-it-is-good-at-right-now]
* creating or updating menu items
* changing availability for an item that is sold out
* looking up orders, users, and restaurant records
* working with fields in Keystone lists without you having to remember the exact mutation name
* acting as a fast admin-side assistant when you already know roughly what you want done
How it works [#how-it-works]
It connects to your live schema [#it-connects-to-your-live-schema]
The assistant uses MCP transport endpoints exposed by the app to inspect your GraphQL schema. That lets it discover list names, input types, and available fields instead of guessing.
It maps your request to the right operation [#it-maps-your-request-to-the-right-operation]
If you ask for something natural, like updating a menu item or finding a reservation, the assistant searches for the right model and resolves the actual GraphQL operation name.
It executes the same API the app already uses [#it-executes-the-same-api-the-app-already-uses]
Queries and mutations run against your existing Keystone GraphQL API. There is no hidden side channel.
It respects your session [#it-respects-your-session]
The assistant uses your current authenticated session. In practice, that means it can only do what your account can already do.
Key pieces in the codebase [#key-pieces-in-the-codebase]
The current assistant flow is built around these parts:
* `app/api/completion/route.ts`
* `app/api/mcp-transport/[transport]/route.ts`
* `features/dashboard/actions/ai-chat.ts`
If you want to customize the assistant, those are the files to start with.
Configuration [#configuration]
You can run the assistant with shared OpenRouter credentials or with user-provided local keys.
Shared keys [#shared-keys]
Set these environment variables:
```bash
OPENROUTER_API_KEY="sk-or-v1-..."
OPENROUTER_MODEL="anthropic/claude-3.5-sonnet"
OPENROUTER_MAX_TOKENS="4000"
```
Local keys [#local-keys]
The dashboard UI also supports storing a per-user OpenRouter key and model choice in local settings.
If you are rolling this out to staff, shared keys are the cleaner default. Local keys are useful when you want power users to bring their own model access.
What to be careful about [#what-to-be-careful-about]
* The assistant is only as safe as the underlying access rules.
* It is best used by managers and admins, not as a substitute for a locked-down permissions model.
* For bulk changes, review the result just like you would review a spreadsheet import.
Good first use cases [#good-first-use-cases]
Try prompts like these inside the dashboard:
* "Mark The Big Stack as unavailable tonight."
* "Show me all waiting parties with a quoted wait longer than 20 minutes."
* "Find menu items tagged as dinner and sort them by name."
* "Create a new modifier called Extra Pickles for the Classic Burger."
What it is not [#what-it-is-not]
It is not a customer-facing chat widget, and it is not a replacement for a finished permissions audit. Think of it as a practical admin operator that already knows your schema.
# External dashboards
To create a custom Restaurant dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Restaurant's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Restaurant codebase before proposing work.
# External storefronts
To create a custom Restaurant storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Restaurant's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Restaurant codebase before proposing work.
# Getting started
This guide prepares an isolated local restaurant evaluation. The onboarding flow creates synthetic StoreSettings, menu data, kitchen stations, floor/section/table records, and payment-provider configuration. It does not create a live restaurant, provider account, hosted demo, or multi-location tenant.
What you need [#what-you-need]
* Node.js 20 or newer
* PostgreSQL
* A `SESSION_SECRET` that is at least 32 characters long
* Stripe keys if you want card checkout on day one
The source expects Stripe variables for card flows. Configuration alone is not payment verification: use test credentials and confirm signatures, replay handling, server-derived totals, failure paths and refunds before enabling a provider.
Install and boot the project [#install-and-boot-the-project]
Clone the repository [#clone-the-repository]
```bash
git clone https://github.com/openshiporg/openfront-restaurant.git
cd openfront-restaurant
```
Install dependencies [#install-dependencies]
```bash
npm install
```
Add your environment variables [#add-your-environment-variables]
Create a `.env` file in the project root.
```bash
# Required
DATABASE_URL="postgresql://username:password@localhost:5432/openfront_restaurant"
SESSION_SECRET="replace-this-with-a-long-random-string-of-32-chars-or-more"
# Recommended for storefront and card payments
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
NEXT_PUBLIC_STRIPE_KEY="pk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
# Optional PayPal adapter setup
NEXT_PUBLIC_PAYPAL_CLIENT_ID="your-paypal-client-id"
PAYPAL_CLIENT_SECRET="your-paypal-client-secret"
PAYPAL_WEBHOOK_ID="your-paypal-webhook-id"
NEXT_PUBLIC_PAYPAL_SANDBOX="true"
# Optional AI assistant
OPENROUTER_API_KEY="sk-or-v1-..."
OPENROUTER_MODEL="anthropic/claude-3.5-sonnet"
OPENROUTER_MAX_TOKENS="4000"
# Optional customer self-signup
PUBLIC_SIGNUPS_ALLOWED="true"
# Optional file storage
S3_BUCKET_NAME="your-bucket"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="your-key"
S3_SECRET_ACCESS_KEY="your-secret"
S3_ENDPOINT="https://your-s3-endpoint"
# Optional email
SMTP_FROM="no-reply@example.com"
SMTP_HOST="smtp.example.com"
SMTP_PORT="587"
SMTP_USER="smtp-user"
SMTP_PASSWORD="smtp-password"
SMTP_STORE_LINK="http://localhost:3000"
```
Start the app [#start-the-app]
```bash
npm run dev
```
This command builds the Keystone schema, applies migrations, and starts the Next.js dev server.
Create your first admin and seed the restaurant [#create-your-first-admin-and-seed-the-restaurant]
Create the initial user [#create-the-initial-user]
If the database is empty, go to:
```text
http://localhost:3000/dashboard/init
```
Create your first admin account there.
Sign in to the dashboard [#sign-in-to-the-dashboard]
After the first user exists, the main dashboard lives at:
```text
http://localhost:3000/dashboard
```
Run onboarding [#run-onboarding]
The sidebar includes onboarding cards for a fresh install. Use that flow to seed:
* store settings
* menu categories and items
* modifiers
* kitchen stations
* floors, sections, and tables
* payment providers
The seed data is restaurant-specific and lives in `features/platform/onboarding/lib/seed.json`.
Open the key surfaces [#open-the-key-surfaces]
Once onboarding finishes, you can jump straight into the main workflows:
* Storefront: `http://localhost:3000/`
* POS: `http://localhost:3000/dashboard/platform/pos`
* Service floor: `http://localhost:3000/dashboard/platform/service-floor`
* KDS: `http://localhost:3000/dashboard/platform/kds`
* Reports: `http://localhost:3000/dashboard/platform/reports`
What gets created during onboarding [#what-gets-created-during-onboarding]
The out-of-the-box seed is intentionally practical. It gives you a burger-restaurant setup with:
* branded store settings and hours
* kitchen stations like Grill, Fryer, Salad, Bar, Dessert, and Prep
* a main floor plus sections
* a table layout with capacities and coordinates
* featured items, burgers, chicken, sides, drinks, and desserts
* Stripe, PayPal, and manual payment-provider records
That is enough to test the storefront, POS, KDS, reports, and floor workflows without building everything from scratch.
Good first checks after boot [#good-first-checks-after-boot]
Check the storefront [#check-the-storefront]
Open the homepage and confirm you can:
* browse categories
* open an item customization modal
* add items to the bag
* open the checkout modal
Check the staff tools [#check-the-staff-tools]
Open the dashboard and make sure you can:
* create a POS order
* see it in Orders
* sync it into the KDS
* move through the payment screen
Check localization [#check-localization]
If you changed currency, locale, or timezone in store settings, confirm those changes show up in:
* storefront prices
* payment screens
* reports
* footer hours
Common setup mistakes [#common-setup-mistakes]
* **Short `SESSION_SECRET`**: the app expects a secret with at least 32 characters.
* **Missing Stripe publishable key**: the checkout modal reads `NEXT_PUBLIC_STRIPE_KEY`, and some storefront components still fall back to `STRIPE_PUBLISHABLE_KEY`, so set both.
* **No store settings**: the storefront depends on the singleton `StoreSettings` record. Onboarding is the easiest way to create it.
* **Expecting PayPal parity on day one**: the adapter is in the codebase, but Stripe is the safer first launch path today.
Where to go next [#where-to-go-next]
# Openfront Restaurant
[Source](https://github.com/openshiporg/openfront-restaurant) · [Catalog](https://openship.org/products/openfront-restaurant)
Openfront Restaurant is the restaurant branch of Openfront. It is not just the ecommerce build with menu labels swapped in. The current codebase covers the core restaurant loop: publish a menu, take orders from the storefront or staff-facing tools, route them into the kitchen, manage the floor, close the check, and report on the shift afterward.
Before using real service or payment data, verify payment handling, cart ownership, API-key enforcement, concurrency controls, build checks, and the deployed order flow.
What ships today [#what-ships-today]
How the order lifecycle works [#how-the-order-lifecycle-works]
A guest or staff member starts the order [#a-guest-or-staff-member-starts-the-order]
A guest can order through the storefront. Staff can start the same order from the POS or service-floor tools. In every case, the system writes a `RestaurantOrder` with structured `OrderItem` records underneath it.
Payment gets attached to the order [#payment-gets-attached-to-the-order]
The storefront creates the order first, then creates a payment record and confirms it. In staff workflows, payments are usually handled at the end of service through the payment screen.
The order enters the kitchen pipeline [#the-order-enters-the-kitchen-pipeline]
Once the order is paid or pushed forward, kitchen tickets are generated per station. Grill items go to Grill, fryer items go to Fryer, expo gets the final coordination view, and so on.
Front of house keeps the floor moving [#front-of-house-keeps-the-floor-moving]
Hosts work the waitlist and seating flow. Servers use the service-floor UI to add items, fire or recall courses, split checks, combine tables, and transfer active checks when service changes hands.
The shift closes with reporting and inventory updates [#the-shift-closes-with-reporting-and-inventory-updates]
When an order reaches `completed`, recipe-linked ingredients can be depleted automatically, payments are recorded, and the reporting views pick up the finished sale.
The main product surfaces [#the-main-product-surfaces]
Storefront [#storefront]
The storefront is a customer-facing menu and ordering experience. It is driven by `StoreSettings`, `MenuCategory`, `MenuItem`, `MenuItemModifier`, `Cart`, and `CartItem`. Guests browse the menu, customize items, choose pickup or delivery, and check out without leaving the page.
POS [#pos]
The POS screen is built for opening orders quickly. Staff can switch between dine-in and takeout, select one or more tables, build a cart, assign courses, flag the order as urgent, and send it into service.
Service floor [#service-floor]
The service-floor screen is where the restaurant starts to feel like restaurant software instead of generic admin UI. It tracks table states, lets staff open a table, add items to an active check, move parties, combine tables, split payments, and handle course timing.
KDS [#kds]
The kitchen display system organizes tickets by station and by status. It supports ticket view, all-day view, overdue timing thresholds, item-level completion, and an expediter gate so expo cannot bump a ticket while prep stations are still working it.
Platform admin [#platform-admin]
The dashboard covers menu setup, store settings, payment providers, gift cards, discounts, onboarding, reports, staffing, and inventory workflows.
Current shape of the platform [#current-shape-of-the-platform]
Current implementation [#current-implementation]
* Storefront menu, cart, checkout modal, account area, and order confirmation flow
* POS order entry with tables, courses, urgency, and special instructions
* Service-floor actions for transfer, combine, split, fire, recall, and payment handoff
* KDS with station tabs, lane filters, all-day view, and item-level readiness tracking
* Menu modeling with images, featured items, modifiers, dietary flags, and meal-period tagging
* Store settings for hours, locale, currency, delivery fees, pickup discounts, and storefront copy
* Inventory, recipe costing, purchase orders, waste logs, and stock movements
* Staff scheduling, tip pools, and labor reporting driven by time entries
* Synthetic onboarding data for local evaluation and MCP assistant source, subject to the family security gates
Current boundaries [#current-boundaries]
`StoreSettings` is a singleton. Although Organization, RestaurantGroup, and Location models exist, current orders, menu, inventory, floor, payment, and staff aggregates are not comprehensively partitioned by those records. Document and deploy this as one restaurant/one operating location unless the owning source adds and tests direct ownership across every aggregate.
* Stripe, PayPal, and manual adapters exist, but provider behavior is not uniform across every restaurant workflow. Verify the selected adapter's session, capture, refund, webhook, replay, and reconciliation path.
* Gift-card redemption has controlled POS behavior. Discount models and administration exist, but source does not establish automatic application across every order path.
* Reservation and waitlist models and commands exist; neither implies SMS delivery or a complete host-stand/provider workflow.
* Current source does not establish offline POS/device/printer guarantees, a durable check/tab and cash-ledger model, loyalty/CRM, delivery-marketplace dispatch, accounting/payroll export, or multi-location tenancy.
* Role, API-key scope, ownership, split-check allocation, and concurrent inventory/payment behavior require release-specific negative and runtime tests.
Stack and architecture [#stack-and-architecture]
* Next.js 16 with the App Router
* React 19
* KeystoneJS 6 for lists, auth, and GraphQL
* PostgreSQL through Prisma
* Stripe, PayPal, and manual payment adapters
* Tailwind CSS and shadcn/ui for the interface layer
* MCP-backed AI actions in the dashboard
Openfront Restaurant keeps the same overall pattern as the original Openfront project: thin app routes, business logic in `features/platform/*` and `features/keystone/*`, and a GraphQL API as the shared contract across storefront, admin, and AI workflows.
Where to go next [#where-to-go-next]
# Orders and service states
The order model is the backbone of Openfront Restaurant. Almost every surface touches it: storefront checkout, POS, service floor, KDS, payments, reporting, and inventory depletion.
Core order statuses [#core-order-statuses]
The `RestaurantOrder` model currently uses these statuses:
* `open`
* `sent_to_kitchen`
* `in_progress`
* `ready`
* `served`
* `completed`
* `cancelled`
That status flow is not just cosmetic. It drives kitchen visibility, table state, reporting, and stock updates.
What an order can contain [#what-an-order-can-contain]
A restaurant order can already store:
* order type and source
* table links for dine-in service
* guest count
* special instructions
* urgency and hold state
* customer name, email, phone, and delivery fields
* courses
* order items
* payments
* discounts and gift cards relationships
* a per-order currency code
How orders move through service [#how-orders-move-through-service]
The order is created [#the-order-is-created]
An order can come from the storefront, POS, or another internal workflow. At creation time, it gets an order number, line items, totals, and context like tables or customer info.
Kitchen work begins [#kitchen-work-begins]
Once payment is confirmed or the order is otherwise pushed forward, kitchen tickets are created per station. At that point the order moves into the kitchen pipeline.
Front of house and kitchen update it together [#front-of-house-and-kitchen-update-it-together]
KDS ticket status and item fulfillment push the order from `sent_to_kitchen` to `in_progress`, then `ready`, then `served`.
Payment closes the order [#payment-closes-the-order]
When the check is settled, the order becomes `completed`. If it is cancelled instead, the status changes accordingly.
Inventory and reporting catch up [#inventory-and-reporting-catch-up]
On completion, recipe-linked ingredients can be depleted automatically and the finished sale shows up in reporting.
Order list views [#order-list-views]
The order list already supports a few operational presets:
* **Kitchen**: open, sent to kitchen, in progress, and ready
* **Expedite**: ready and served
* **Cashier**: served and completed
You can also filter directly by status or source.
Detail and fulfillment pages [#detail-and-fulfillment-pages]
Openfront Restaurant includes dedicated order pages for:
* reviewing an order in detail
* handling service management and fulfillment
* jumping into payment for a live check
Those screens are where staff can move from a simple list to the actual work of handling the table.
Example query [#example-query]
```graphql
query KitchenOrders {
restaurantOrders(
where: { status: { in: ["open", "sent_to_kitchen", "in_progress", "ready"] } }
orderBy: { createdAt: desc }
) {
id
orderNumber
status
orderType
isUrgent
tables {
tableNumber
}
}
}
```
Important hooks in the model [#important-hooks-in-the-model]
A few useful behaviors already happen automatically:
* dine-in orders can mark linked tables as occupied on create
* completed or cancelled dine-in orders can move tables into cleaning
* completed orders can trigger ingredient depletion when recipes exist
That means order status is already doing real platform work behind the scenes.
If you are debugging restaurant behavior, start with the order record. In practice, it is the shared source of truth between guest ordering, staff service, kitchen execution, and reporting.
# Payment providers
Openfront Restaurant uses adapter-based payment providers. The model is simple: each provider record stores the function names that should handle create, capture, refund, status, dashboard-link, and webhook work.
In the current build, those adapters live in:
* `features/integrations/payment/stripe.ts`
* `features/integrations/payment/paypal.ts`
* `features/integrations/payment/manual.ts`
What is already working well [#what-is-already-working-well]
Stripe [#stripe]
Stripe is the strongest path today.
It is used for:
* storefront card checkout
* payment intents
* capture and status checks
* webhook verification through the generic webhook mutation path
Manual and cash flows [#manual-and-cash-flows]
Manual payments are useful for:
* cash at the register
* cash at the table
* internal testing
* mixed or split payment situations
PayPal [#paypal]
A PayPal adapter already exists, and parts of the storefront UI are ready for it. The restaurant product just is not as uniformly standardized around PayPal as it is around Stripe yet.
How the provider model works [#how-the-provider-model-works]
Create or seed the provider record [#create-or-seed-the-provider-record]
Onboarding already creates records for Stripe, PayPal, and Manual.
The runtime resolves the adapter [#the-runtime-resolves-the-adapter]
When the app needs to create or capture a payment, it looks up the provider record and calls the named adapter function through `paymentProviderAdapter.ts`.
Payments attach to restaurant orders [#payments-attach-to-restaurant-orders]
Payment records are linked to `RestaurantOrder`, so the restaurant order stays at the center of the flow.
Webhooks update the order state [#webhooks-update-the-order-state]
The Stripe webhook route forwards into the generic webhook mutation, which verifies the event through the adapter and then updates payment and order status.
Provider fields you will see [#provider-fields-you-will-see]
A provider record stores fields like:
* `createPaymentFunction`
* `capturePaymentFunction`
* `refundPaymentFunction`
* `getPaymentStatusFunction`
* `generatePaymentLinkFunction`
* `handleWebhookFunction`
* credentials and metadata
That makes the integration layer flexible without hardcoding every processor directly into route handlers.
Launch advice [#launch-advice]
If you want the least risky setup for a real launch, use:
* **Stripe** for online card checkout
* **Manual** for cash or internal fallback flows
Then add PayPal once you are happy with the rest of the payment pipeline.
One honest note: the adapter layer is further along than the end-to-end payment parity across every restaurant workflow. Stripe is the safest primary path today.
# Promotions and gift cards
Openfront Restaurant already includes discount and gift-card models, plus admin screens for managing them. The important detail is that the two are not equally finished.
Gift cards are further along [#gift-cards-are-further-along]
Gift cards already have:
* a `GiftCard` model with value and balance
* a `GiftCardTransaction` model for the ledger
* admin management screens
* lookup and redemption support inside the POS payment flow
* support for partial payment against the remaining balance on a check
That makes gift cards more than just placeholder data.
Discounts are modeled, but not as wired through [#discounts-are-modeled-but-not-as-wired-through]
Discounts already have:
* `Discount` and `DiscountRule` models
* admin list and detail pages
* reporting fields that can surface discount totals on orders
What they do not have yet is the same level of end-to-end restaurant-flow wiring that gift cards have. If you need fully automatic coupon application in storefront or staff payment flows, plan to finish that last mile before launch.
Practical read on the current state [#practical-read-on-the-current-state]
* **Gift cards**: useful now
* **Discount management**: structurally present
* **Discount automation**: still needs more work
If you are deciding what to trust first, trust gift cards before you trust automated restaurant discounts.
# Service floor
If the POS is for opening checks, the service-floor screen is for running service.
This is one of the most restaurant-specific parts of the product. It shows table state, active checks, course timing, split logic, and table movement in one place, which is exactly where a generic admin app usually falls apart.
What the service-floor screen covers [#what-the-service-floor-screen-covers]
* live table states for available, occupied, reserved, and cleaning
* one-sheet access to the active order on a table
* quick add-item flow for an open check
* split check by guest count
* split check by selected items
* combine multiple tables onto one order
* transfer a check from one table to another
* fire a pending course into the kitchen
* recall a course that should wait
* jump into payment when the table is ready to close
How the workflow usually plays out [#how-the-workflow-usually-plays-out]
Open the table [#open-the-table]
Tap a table to see whether it already has an active check. If it does not, staff can start service from there.
Add items during service [#add-items-during-service]
The service-floor sheet can add more menu items to the open order without sending the staff member back to the original POS flow.
Control course timing [#control-course-timing]
Pending courses can be fired when the table is ready. If timing changes, a course can be recalled back to pending.
Handle check changes cleanly [#handle-check-changes-cleanly]
When a party moves, you can transfer the order. When a group expands, you can combine tables. When payment gets messy, you can split by guest or by item.
Close the table [#close-the-table]
When service is done, hand off to the payment screen and complete the order. Table status can then move into cleaning before it returns to available.
Table states in the current build [#table-states-in-the-current-build]
* `available`
* `occupied`
* `reserved`
* `cleaning`
Those states are used across the service-floor view, waitlist flow, and order hooks.
Why this screen matters [#why-this-screen-matters]
Restaurant software gets hard once the dining room changes in real time. The service-floor page already handles several of the awkward parts that matter in actual service:
* parties moving tables
* multiple tables merging into one check
* separate guests paying separately
* kitchen timing that does not match order entry timing
That is a big part of what makes Openfront Restaurant feel like its own product instead of a themed dashboard.
The service-floor screen also has its own API summary route at `/api/platform/service-floor`, which is useful if you want to build supporting displays or staff tools around the same data.
What still needs more polish [#what-still-needs-more-polish]
* Reservation handling is lighter here than waitlist and table-service handling.
* Some teams will still want a more visual floor-plan editor for layout changes.
* Real-time updates are good enough for active use, but there is still room to tighten the live-sync story further.
# Store settings
`StoreSettings` is a singleton in Openfront Restaurant, and it quietly powers a lot of the product.
This is where you control how the restaurant looks to guests and how money, time, and order defaults behave across the app.
What lives in store settings [#what-lives-in-store-settings]
* restaurant name, tagline, address, phone, and email
* currency code and locale
* timezone and country code
* operating hours
* delivery fee and delivery minimum
* pickup discount
* estimated pickup and delivery timing
* hero headline, subheadline, and promo banner
* rating and review count display data
Why it matters [#why-it-matters]
The same record feeds multiple surfaces:
* storefront hero copy and banner
* footer contact information and hours
* currency formatting in the storefront and reports
* timezone-aware hour display
* delivery and pickup math in checkout
Recommended setup order [#recommended-setup-order]
Start with identity [#start-with-identity]
Add the name, tagline, address, phone number, and email so the storefront stops looking generic.
Set localization correctly [#set-localization-correctly]
Choose currency, locale, timezone, and country before you start testing prices and reports.
Set your ordering defaults [#set-your-ordering-defaults]
Delivery fee, minimums, pickup discount, and estimated timing all feed directly into the guest ordering flow.
Save and verify the storefront [#save-and-verify-the-storefront]
Check the homepage, footer, and checkout modal to make sure the settings are showing up where you expect.
What changed recently [#what-changed-recently]
The restaurant build now has better currency and localization support than the early draft docs suggested. Pricing, formatting, and several reporting surfaces already read from store settings instead of assuming a fixed US-only setup.
# External dashboards
To create a custom Salon dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains Salon's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Salon codebase before proposing work.
# External storefronts
To create a custom Salon storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement Salon's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied Salon codebase before proposing work.
# Openfront Salon
[Source](https://github.com/openshiporg/openfront-salon) · [Catalog](https://openship.org/products/openfront-salon)
Openfront Salon is a salon/spa storefront and operator workspace for service discovery, stylist profiles, booking and intake, client accounts, appointments, schedules/resources, checkout and retail products, packages/memberships, gift cards, loyalty, waitlist, inventory, and commissions.
Current source has product-specific GraphQL commands, but it does not have a strong organization/location membership tenant model. Dashboard capability grants broad access in several list filters. Verify operator scope, client privacy, booking/resource concurrency, money, and provider state before using real records.
Customer and operator surfaces [#customer-and-operator-surfaces]
Customer routes cover home, services and detail, team/stylist detail, booking, appointments/account, gallery, reviews, contact, and gift cards. Operator routes under `/dashboard/platform` cover appointments, checkout, clients, catalog, benefits, inventory, schedules, resources, commissions, administration, engagement/waitlist, reports, and integrations.
The main workflow is service/variant/stylist discovery -> availability -> appointment/intake -> resource allocation -> service and retail/package usage -> checkout/payment -> commission, loyalty, inventory, and customer history. Accepted service and price facts belong in appointment and transaction line snapshots rather than mutable catalog relations.
Schema and scope [#schema-and-scope]
The graph includes Location, service categories/services/variants/packages, stylists/schedules, resources, clients/notes/formulas, appointments and lines, intake forms/submissions, memberships/packages/usage, products/sales/inventory movements, salon transactions/lines, providers/sessions/payments/webhook events, gift cards, loyalty/referrals, commissions/payouts, messages, reviews, media, waitlist, and widget settings.
Location is a relationship root, but current source has no Organization or LocationMembership authorization layer. `canOperateSalon` is largely dashboard access and several read filters expose broad operator data. Location IDs and submitted relationships still need explicit same-location validation in every command; generated visibility alone is not a complete tenant boundary.
Protect client notes, intake answers, color formulas, messages, payment details, provider credentials, and history with field-level policy. Third-party clients should use narrow projections and commands rather than generated list CRUD.
Controlled GraphQL boundary [#controlled-graphql-boundary]
The current source does register a salon-specific extension in `features/keystone/mutations/index.ts`. It includes:
* availability, stylist, and `salonOperationsWorkspace` projections;
* appointment booking, deposit/payment, and lifecycle transitions;
* `completeAppointmentCheckout` and payment processing;
* gift-card purchase/application and package redemption;
* waitlist entry/transitions and stylist schedule operations;
* commission calculation and payout transitions;
* inventory adjustment and resource availability;
* onboarding.
These operations are the preferred lifecycle boundary. Remaining work is not absence of a GraphQL extension; it is consistent location ownership, idempotency, transaction/concurrency behavior, immutable settlement/refund evidence, and closure of equivalent generated/server-action paths.
Prove simultaneous stylist/resource bookings, appointment terminal-state denial, package/membership balance consumption, gift-card replay, inventory underflow, checkout amount/currency, commission arithmetic, and refund/reversal behavior against the target database.
Onboarding and local evaluation [#onboarding-and-local-evaluation]
`runSalonOnboarding` and its platform action create synthetic Morrow & Mane identity, locations, services, stylists, schedules, resources, clients, intake/forms, memberships/packages, products, messages, appointments, gallery, and manual/Stripe provider records. Run it twice on an isolated database, inspect location relationships and status, then test customer ownership, booking, package use, checkout, inventory, commission, and operator denial. The seed is not a hosted demo or provider account.
Payments and integrations [#payments-and-integrations]
The code-owned payment registry allowlists manual and Stripe adapters. Stripe ingress is `/api/payments/webhooks/[provider]`; verify raw-body signatures, event replay, server-owned amount/currency, session ownership, capture/refund mapping, and reconciliation. Manual settlement must remain operator-only and cannot unlock a public paid state without accountable evidence.
Message logs, templates, widget settings, reviews, and gallery records do not establish SMS/email delivery, marketing automation, booking-widget isolation, review syndication, or media processing. Each external effect needs a typed adapter, secret isolation, signed ingress, durable attempts, consent/retention policy, and operator-visible failure state.
Deployment and limitations [#deployment-and-limitations]
Railway builds the application and deploys migrations when starting. The package's lint script uses the removed `next lint` command under Next.js 16, so it is not a valid release gate until migrated to the ESLint CLI. Placeholder values in `.env.example` are not deployable credentials.
Before real clients or payments, run reviewed migrations, current schema/type/tests/build, cross-client and cross-location negatives, booking races, webhook replay, refund/reversal, gift-card/package balances, inventory, commission, token/session, responsive browser, backup restoration, monitoring, and incident checks.
Current source does not establish strong multi-location tenancy, payment settlement, external messaging, payroll, tax, identity, professional-licensing, health/safety, privacy, accessibility, or operational certification. A rendered booking or recorded payment row is not evidence that the full external workflow completed.
# External dashboards
To create a custom UIKit dashboard, copy the skill and give it to your LLM. The skill will explain how `features/dashboard` supplies the Keystone administration shell while `features/platform` contains UIKit's product-specific operator experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied UIKit codebase before proposing work.
# External storefronts
To create a custom UIKit storefront, copy the skill and give it to your LLM. The skill will explain how `features/storefront` and the customer-facing app routes implement UIKit's current experience.
After that source orientation, it will ask what you dislike, what should stay, what workflows and users matter, and what the replacement should look and feel like. It then traces the exact routes, feature slices, GraphQL calls, schema, access rules, sessions, and provider boundaries in the supplied UIKit codebase before proposing work.
# Openfront UIKit
Openfront UIKit is a self-hosted component-kit catalog and private registry foundation. It models kits, releases and files, compatibility and examples, accessibility/review evidence, registry keys, checkout orders, payments and access grants.
Before enabling paid access, verify manual-payment restrictions, immutable settlement, webhook replay, and entitlement behavior in the deployed environment.
Architecture and schema [#architecture-and-schema]
Catalog/release/design-system records are globally scoped. Customer commerce is user/product scoped through Order, KitAccess, and RegistryApiKey; there is no organization/team tenant graph. The graph includes brand settings, kit products, releases, component files, package exports, compatibility targets, usage examples, integration recipes, screenshots, accessibility audits, review workflows and changelogs. Commerce records include orders/items, provider/session/payment/event records and kit access. Registry API keys control package access.
Public routes cover the kit catalog, `/kits/[slug]`, kit checkout, account token, and registry docs. `/api/registry/[code]` validates the code, product, rate limit, key/access summary, release/version, and manifest integrity before returning files. Operator routes cover catalog, design systems, components, quality, releases, API keys, orders, payments, refunds, access, and audit.
Main workflow [#main-workflow]
An operator publishes a kit release and its immutable files/exports. A user selects a kit, completes a verified checkout, receives a scoped entitlement, and creates or rotates a registry key. Registry requests validate both key and active access before returning a release artifact.
Bounded GraphQL operations [#bounded-graphql-operations]
Public catalog and operator projections expose kit/release/access/order state without returning key hashes or private files. Named operations submit, publish, deprecate, and review releases; create/revoke registry keys and kit access; create/complete/refund kit orders; handle payment webhooks; settle permitted offline orders; edit products; and run onboarding. Registry delivery independently checks the key, user, kit entitlement, release, and artifact before returning content.
Setup and onboarding [#setup-and-onboarding]
Use a disposable PostgreSQL database with synthetic packages and test credentials. `runUIKitOnboarding` creates local catalog, design-system, token, component, release, quality, and commerce data. Run it twice on an isolated current database, then verify catalog projections, review/publish/deprecate transitions, key revocation, checkout, operator settlement, refund, and entitlement access. Seeded kits are not a hosted registry or package service.
Integrations [#integrations]
The code-owned payment registry contains Stripe, PayPal, and manual adapters plus signed webhook ingress. Adapter source is not settlement proof. Manual capture/refund/status intentionally throws and must remain an operator-recorded offline settlement boundary; it cannot grant public paid access by itself.
External package hosting, artifact signing/provenance, malware scanning, CDN delivery, package-manager publication, and supply-chain attestation are not implemented by registry/file records.
Security and deployment [#security-and-deployment]
Hash registry keys, restrict them to the entitled user/kit, support revocation and rotation, and avoid exposing private files through predictable URLs. Settlement and entitlement creation need one transaction, verified provider events, and replay protection. Railway builds then migrates at start; the package's `next lint` script is invalid under Next.js 16.
Deploy only after reviewed migrations, current schema/type/tests/build, catalog/release lifecycle, key/access revocation, rate limits, checkout/refund/webhook replay, artifact integrity, retention, backup restoration, and responsive catalog/operator checks. A registry key or hash-checked manifest is not proof of package signing, malware safety, payment settlement, accessibility quality, compatibility, or supply-chain certification.
# Dashboard customization
Openship's dashboard lives in the same repository as its Keystone and routing source. Custom work should keep App Router pages thin and place shop, channel, match, order and API-key behavior in feature slices and domain operations.
Keep these boundaries [#keep-these-boundaries]
* route pages parse navigation and delegate;
* authenticated screens load only the user's scoped records;
* provider credentials stay in server-side adapter boundaries;
* order placement, cancellation and tracking transitions use named domain operations;
* UI controls do not call arbitrary provider URLs supplied by the browser;
* loading, empty, permission, partial-failure and retry states stay visible.
A separate dashboard can be built against GraphQL, but it does not remove the need for session/API-key scopes, tenant filters, CSRF/origin policy and narrow mutations. Use the [generated API contract](/docs/openship/ecommerce/api-reference) from the same source revision.
# Operator workspace
Openship's purpose-built dashboard is an operator workspace for user-owned order routing. The current application uses Next.js 16, Keystone 6, React 19, GraphQL, Prisma, and PostgreSQL. It is not a customer storefront or payment platform.
The workspace exposes routing records and calls adapter operations. A visible order, match, purchase ID, or tracking row does not prove external fulfillment. Verify the selected adapter, callback, idempotency, and reconciliation path with synthetic systems before connecting real credentials.
Current routes [#current-routes]
* `/dashboard/platform/orders` — inspect imported orders, lines, match state, cart items, downstream purchases, and tracking;
* `/dashboard/platform/shops` — configure user-owned order sources and ShopPlatform handlers;
* `/dashboard/platform/channels` — configure fulfillment destinations and ChannelPlatform handlers;
* `/dashboard/platform/matches` — search both sides and map exact shop variants to channel variants;
* `/dashboard/platform/api-keys` — create and revoke machine credentials;
* `/dashboard/[listKey]` — generated administration for permitted schema records.
The first user is initialized at `/dashboard/init`, not `/init` or `/platform`. Current source has no product onboarding wizard or demo seed; shop/channel/link/match setup is manual.
Task workflow [#task-workflow]
1. Create the first operator and sign in.
2. Configure one synthetic shop against the compiled Shopify/Openfront handler or an isolated custom endpoint.
3. Configure one synthetic channel against a compiled handler or isolated endpoint.
4. Create the user-owned Link between those records.
5. Search source and destination catalogs and create an exact variant Match.
6. Import or select a synthetic shop order.
7. Use bounded match/cart/purchase commands to create the downstream purchase.
8. Authenticate tracking/cancellation events and reconcile all three systems.
The current custom GraphQL layer includes product/order searches, match operations, cart and placement commands, purchase/cancellation operations, and webhook management. Generated list CRUD is not a replacement for those transition paths.
Tenancy and credentials [#tenancy-and-credentials]
Openship is scoped directly to the owning User; it does not have an organization/workspace tenant graph. Related shop, channel, order, link, match, and item IDs need explicit same-user validation in every custom resolver and HTTP handler.
API keys have hashed token material and stored scopes, but scope names and resolver coverage must be verified operation by operation. Shop and Channel access/refresh tokens are stored as model text fields in current source and need stronger field denial/encryption before real provider use.
Adapter boundary [#adapter-boundary]
Compiled shop and channel adapters currently cover Shopify and Openfront. Database-selected HTTP URLs or function paths can also be executed. No other provider name is built-in merely because a platform row or UI example can be created.
Before enabling configurable execution, add strict egress and import allowlists, credential isolation, bounded payload validation, signed callbacks, provider idempotency, durable attempts/retries, dead-letter visibility, and reconciliation. Current webhook handlers do not all share a durable delivery ledger.
Deployment shape [#deployment-shape]
`npm run dev` and `npm run build` both deploy checked-in migrations before starting/building Next.js. Use a deliberate `DATABASE_URL`; split migration from immutable build if your deployment promotes the same artifact between environments. The checked-in `lint` script invokes the removed Next.js 16 `next lint` command, and the package has no test or typecheck script, so do not represent those gates as passing until the owning repository adds current commands.
Run Openship behind trusted HTTPS with managed session/provider secrets, restricted internal network access, backups and restore tests, request/body limits, centralized logs, and adapter monitoring. Exercise cross-user denial, malformed provider responses, duplicate order webhooks, duplicate purchases, partial channel failure, cancellation, and tracking reconciliation for the exact release.
# Create Custom Channel
Overview [#overview]
Instead of a compiled Shopify/Openfront channel adapter, a ChannelPlatform can select custom HTTP endpoints that Openship calls for fulfillment operations. The endpoint must implement the documented request/response contract and remain responsible for its own provider-specific behavior.
Current Openship source executes database-selected URLs without a complete destination allowlist, private-address/redirect defense, credential vault, or durable attempt ledger. Add those controls and prove idempotency, signed callbacks, retries, and reconciliation before using this extension with real provider credentials or purchases. The docs endpoints are POST-only in-memory examples, not a supplier or 3PL.
How Custom Integrations Work [#how-custom-integrations-work]
When you create a channel platform in Openship, instead of setting function values to built-in slugs like "shopify", you can provide HTTP URLs. Openship will make POST requests to these endpoints with the required parameters and expect specific response formats.
For example:
* **Built-in**: `searchProductsFunction: "shopify"`
* **Custom**: `searchProductsFunction: "https://your-fulfillment-api.com/api/search-products"`
Required HTTP Endpoints [#required-http-endpoints]
Your custom channel integration must implement the following HTTP endpoints that match the function interface from the channel integration guide:
Product Search - `/api/search-products` [#product-search---apisearch-products]
**Purpose**: Openship uses this endpoint to search for products available for purchase when setting up product matches, allowing users to find fulfillment options for their shop products
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ------------- | ----------------------------------------- | -------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `searchEntry` | `string` | Search query |
| `after?` | `string` | Pagination cursor |
**Response Format**:
| Field | Type | Description |
| ---------- | --------------------------------------------- | ------------------------------------ |
| `products` | `Product[]` | Array of purchasable product objects |
| `pageInfo` | `{ hasNextPage: boolean; endCursor: string }` | Pagination information |
Implementation
Response
```typescript
// POST /api/search-products
export default async function handler(req, res) {
const { platform, searchEntry, after } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your custom product catalog (suppliers, 3PL inventory, etc.)
const allProducts = [
{
image: "https://via.placeholder.com/300x300/FF0000/FFFFFF?text=PIPE",
title: "Hooli XYZ Compression Device",
productId: "hooli001",
variantId: "white",
price: "299.99",
availableForSale: true,
inventory: 150,
inventoryTracked: true,
productLink: "https://hooli-supply.com/products/xyz-compression",
cursor: "eyJpZCI6Imhvb2xpMDAxIn0="
},
{
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=STORAGE",
title: "Electronics Storage Box",
productId: "storage001",
variantId: "crypto-box",
price: "2000.00",
availableForSale: true,
inventory: 8,
inventoryTracked: true,
productLink: "https://demo-supplier.com/products/crypto-device",
cursor: "eyJpZCI6InNldGVjMDAxIn0="
},
{
image: "https://via.placeholder.com/300x300/4B0082/FFFFFF?text=HACK",
title: "Wireless Mouse - Blue",
productId: "phreaks001",
variantId: "blue-box",
price: "500.00",
availableForSale: true,
inventory: 12,
inventoryTracked: true,
productLink: "https://demo-store.com/products/wireless-mouse",
cursor: "eyJpZCI6Im1vdXNlMDAxIn0="
}
];
let filteredProducts = allProducts;
// Filter by search term if provided
if (searchEntry) {
filteredProducts = allProducts.filter(product =>
product.title.toLowerCase().includes(searchEntry.toLowerCase())
);
}
// Simple pagination
let startIndex = 0;
if (after) {
const decodedCursor = JSON.parse(Buffer.from(after, 'base64').toString());
startIndex = allProducts.findIndex(p => p.productId === decodedCursor.id) + 1;
}
const paginatedProducts = filteredProducts.slice(startIndex, startIndex + 10);
const hasNextPage = startIndex + 10 < filteredProducts.length;
const endCursor = paginatedProducts.length > 0
? paginatedProducts[paginatedProducts.length - 1].cursor
: null;
return res.status(200).json({
products: paginatedProducts,
pageInfo: {
hasNextPage,
endCursor
}
});
}
```
```json
{
"products": [
{
"image": "https://via.placeholder.com/300x300/FF0000/FFFFFF?text=PIPE",
"title": "Hooli XYZ Compression Device",
"productId": "hooli001",
"variantId": "white",
"price": "299.99",
"availableForSale": true,
"inventory": 150,
"inventoryTracked": true,
"productLink": "https://hooli-supply.com/products/xyz-compression",
"cursor": "eyJpZCI6Imhvb2xpMDAxIn0="
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "eyJpZCI6Imhvb2xpMDAxIn0="
}
}
```
Get Single Product - `/api/get-product` [#get-single-product---apiget-product]
**Purpose**: Openship uses this endpoint to fetch detailed product information from channel platforms when creating product matches, ensuring accurate pricing and availability data for automated purchasing
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ------------ | ----------------------------------------- | -------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `productId` | `string` | Product identifier |
| `variantId?` | `string` | Variant identifier |
**Response Format**:
| Field | Type | Description |
| --------- | --------- | ------------------------------------------- |
| `product` | `Product` | Single product object with purchase details |
Implementation
Response
```typescript
// POST /api/get-product
export default async function handler(req, res) {
const { platform, productId, variantId } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your product catalog
const products = {
"hooli001": {
white: {
image: "https://via.placeholder.com/300x300/FF0000/FFFFFF?text=PIPE",
title: "Hooli XYZ Compression Device - White",
productId: "hooli001",
variantId: "white",
price: "299.99",
availableForSale: true,
inventory: 150,
inventoryTracked: true,
productLink: "https://hooli-supply.com/products/xyz-compression"
}
},
"storage001": {
"crypto-box": {
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=STORAGE",
title: "Electronics Storage Box - Crypto Box",
productId: "storage001",
variantId: "crypto-box",
price: "2000.00",
availableForSale: true,
inventory: 8,
inventoryTracked: true,
productLink: "https://demo-supplier.com/products/crypto-device"
}
}
};
const product = products[productId]?.[variantId || 'default'];
if (!product) {
return res.status(404).json({ error: "Product not found" });
}
return res.status(200).json({ product });
}
```
```json
{
"product": {
"image": "https://via.placeholder.com/300x300/FF0000/FFFFFF?text=PIPE",
"title": "Hooli XYZ Compression Device - White",
"productId": "hooli001",
"variantId": "white",
"price": "299.99",
"availableForSale": true,
"inventory": 150,
"inventoryTracked": true,
"productLink": "https://hooli-supply.com/products/xyz-compression"
}
}
```
Create Purchase - `/api/create-purchase` [#create-purchase---apicreate-purchase]
**Purpose**: Openship uses this endpoint to automatically create purchases on channel platforms when orders are received from connected shops, enabling automated order fulfillment workflow
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ----------- | ----------------------------------------- | -------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `cartItems` | `CartItem[]` | Items to purchase |
| `shipping?` | `ShippingAddress` | Shipping address |
| `notes?` | `string` | Order notes |
**Response Format**:
| Field | Type | Description |
| ------------- | ------------ | ---------------------------------- |
| `purchaseId` | `string` | Unique identifier for the purchase |
| `orderNumber` | `string` | Human-readable order number |
| `totalPrice` | `string` | Total cost of the purchase |
| `invoiceUrl` | `string` | URL to the purchase invoice |
| `lineItems` | `LineItem[]` | Items included in the purchase |
| `status` | `string` | Current purchase state |
Implementation
Response
```typescript
// POST /api/create-purchase
export default async function handler(req, res) {
const { platform, cartItems, shipping, notes } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
try {
// Generate unique purchase ID (in real app, use proper ID generation)
const purchaseId = `PO-${Date.now()}`;
const orderNumber = `#${purchaseId}`;
// Calculate total price
const totalPrice = cartItems.reduce((sum, item) => {
return sum + (parseFloat(item.price) * item.quantity);
}, 0).toFixed(2);
// In a real implementation, you would:
// 1. Create the purchase in your fulfillment system
// 2. Send order to your supplier/3PL
// 3. Update inventory levels
// 4. Generate invoices/receipts
// For demo purposes, let's simulate different responses based on products
const hasCryptoEquipment = cartItems.some(item => item.variantId?.includes('crypto') || item.variantId?.includes('setec'));
if (hasCryptoEquipment) {
// Special handling for crypto/security equipment
return res.status(200).json({
purchaseId,
orderNumber,
totalPrice,
invoiceUrl: `https://demo-supplier.com/invoices/${purchaseId}`,
lineItems: cartItems.map(item => ({
id: `line_${Date.now()}_${Math.random()}`,
title: item.name || `Product ${item.variantId}`,
quantity: item.quantity,
variantId: item.variantId
})),
status: "pending_security_clearance" // Crypto equipment needs clearance
});
}
// Regular purchase flow
const processedLineItems = cartItems.map(item => ({
id: `line_${Date.now()}_${Math.random()}`,
title: item.name || `Product ${item.variantId}`,
quantity: item.quantity,
variantId: item.variantId
}));
// Simulate email notification to supplier
console.log(`📧 Purchase Order Created: ${orderNumber}`);
console.log(`📦 Items:`, processedLineItems);
console.log(`🚚 Ship to: ${shipping?.firstName} ${shipping?.lastName}`);
console.log(`💰 Total: $${totalPrice}`);
return res.status(200).json({
purchaseId,
orderNumber,
totalPrice,
invoiceUrl: `https://your-fulfillment-portal.com/invoices/${purchaseId}`,
lineItems: processedLineItems,
status: "processing"
});
} catch (error) {
console.error('Purchase creation failed:', error);
return res.status(500).json({
error: "Purchase creation failed",
details: error.message
});
}
}
```
```json
{
"purchaseId": "PO-1705123456789",
"orderNumber": "#PO-1705123456789",
"totalPrice": "299.99",
"invoiceUrl": "https://your-fulfillment-portal.com/invoices/PO-1705123456789",
"lineItems": [
{
"id": "line_1705123456789_0.123",
"title": "Hooli XYZ Compression Device",
"quantity": 1,
"variantId": "white"
}
],
"status": "processing"
}
```
Create Webhook - `/api/create-webhook` [#create-webhook---apicreate-webhook]
**Purpose**: Openship uses this endpoint to set up real-time webhooks that notify the system when purchases are fulfilled or cancelled on channel platforms, enabling automatic tracking updates to customers
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ---------- | ----------------------------------------- | --------------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `endpoint` | `string` | Webhook URL |
| `events` | `string[]` | Event types to subscribe to |
**Response Format**:
| Field | Type | Description |
| ---------- | ----------- | -------------------------------- |
| `webhooks` | `Webhook[]` | Array of created webhook objects |
Implementation
Response
```typescript
// POST /api/create-webhook
export default async function handler(req, res) {
const { platform, endpoint, events } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
try {
const webhooks = [];
// Map Openship events to your system's events
const eventMapping = {
"TRACKING_CREATED": "fulfillment.shipped",
"ORDER_CANCELLED": "purchase.cancelled",
"ORDER_CHARGEBACKED": "purchase.disputed"
};
for (const event of events) {
const internalEvent = eventMapping[event] || event;
// In a real implementation, you would register these webhooks
// with your fulfillment system/3PL API
const webhookId = `webhook_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
console.log(`🔗 Registering webhook: ${internalEvent} -> ${endpoint}`);
// Simulate webhook registration
webhooks.push({
id: webhookId,
endpoint: {
callbackUrl: endpoint
},
events: [internalEvent],
active: true,
created_at: new Date().toISOString()
});
}
return res.status(200).json({ webhooks });
} catch (error) {
console.error('Webhook creation failed:', error);
return res.status(500).json({
error: "Webhook creation failed",
details: error.message
});
}
}
```
```json
{
"webhooks": [
{
"id": "webhook_1705123456789_abc123def",
"endpoint": {
"callbackUrl": "https://your-openship.com/webhook/channel"
},
"events": ["fulfillment.shipped"],
"active": true,
"created_at": "2025-01-22T10:30:00Z"
}
]
}
```
Webhook Event Handlers [#webhook-event-handlers]
Your custom channel should implement webhook handlers to notify Openship of fulfillment events:
Fulfillment Tracking Handler - `/api/webhook/tracking-created` [#fulfillment-tracking-handler---apiwebhooktracking-created]
**Purpose**: Openship uses this webhook to receive tracking information when purchases are shipped from channel platforms, automatically forwarding tracking details to customers and updating order status
Implementation
```typescript
// POST /api/webhook/tracking-created
export default async function handler(req, res) {
const { event, headers } = req.body;
// Verify webhook authenticity (implement your own verification)
if (!verifyWebhookSignature(headers)) {
return res.status(403).json({ error: "Invalid signature" });
}
// Transform your fulfillment event to Openship format
const fulfillment = {
id: event.fulfillment_id,
orderId: event.order_id,
status: "success",
trackingCompany: event.shipping_carrier,
trackingNumber: event.tracking_number,
trackingUrl: event.tracking_url,
purchaseId: event.purchase_id,
lineItems: event.shipped_items.map(item => ({
id: item.line_item_id,
title: item.product_name,
quantity: item.quantity_shipped,
variantId: item.variant_id,
productId: item.product_id
})),
createdAt: event.shipped_at,
updatedAt: event.updated_at
};
return res.status(200).json({ fulfillment, type: "fulfillment_created" });
}
function verifyWebhookSignature(headers) {
// Implement your webhook signature verification here
// This could be HMAC, JWT, or other verification method
return true; // Simplified for demo
}
```
Testing Your Custom Channel [#testing-your-custom-channel]
We've built a demo channel integration right into this documentation that you can use to test the complete Openship workflow before implementing your own custom channel.
Step 1: Create a Demo Channel Platform [#step-1-create-a-demo-channel-platform]
1. In your Openship dashboard, navigate to **Platform > Channels**
2. Click **"Add Platform"**
3. In the **"Create Channel Platform"** dialog:
* Select **"Demo"** from the Platform dropdown
* The Name field will auto-fill with **"Demo Channel"**
* Click **"Create Platform"**
This creates a platform that points to our demo channel endpoints at `https://docs.openship.org/api/demo/channel/*`
Step 2: Connect Your Demo Channel [#step-2-connect-your-demo-channel]
1. After creating the platform, click **"Create Channel"**
2. In the **"Create Channel"** dialog:
* **Platform**: Select "Demo Channel Platform"
* **Name**: "Demo" (or any name you prefer)
* **Domain**: "local"
* **Access Token**: "demo\_token"
3. Click **"Create Channel"**
Step 3: Test the Demo Integration [#step-3-test-the-demo-integration]
Once connected, you can test the complete workflow:
* Browse products available for purchase from our demo supplier
* Test product matching between shops and channels
* Create demo purchases and track their status
* Experience automated order fulfillment
Step 4: Replace with Your Implementation [#step-4-replace-with-your-implementation]
When you're ready to use your custom channel:
1. Go to your Demo Channel Platform settings
2. Replace the demo URLs with your actual implementation:
```
# Replace these demo URLs:
https://docs.openship.org/api/demo/channel/search-products
https://docs.openship.org/api/demo/channel/get-product
https://docs.openship.org/api/demo/channel/create-purchase
https://docs.openship.org/api/demo/channel/create-webhook
# ... and other endpoints
# With your URLs:
https://your-fulfillment-api.com/api/search-products
https://your-fulfillment-api.com/api/get-product
https://your-fulfillment-api.com/api/create-purchase
https://your-fulfillment-api.com/api/create-webhook
# ... and so on
```
This approach lets you learn how Openship works with our demo data first, then seamlessly transition to your own implementation.
# Create Custom Shop
Overview [#overview]
Instead of a compiled Shopify/Openfront shop adapter, a ShopPlatform can select custom HTTP endpoints that Openship calls for source catalog and order operations. The endpoint must implement the documented request/response contract and remain authoritative for its own products and orders.
Current Openship source executes database-selected URLs without a complete destination allowlist, private-address/redirect defense, credential vault, or durable attempt ledger. Add those controls and prove ownership, idempotency, signed callbacks, retries, and reconciliation before using this extension with real store credentials or orders. The docs endpoints are POST-only in-memory examples, not a commerce platform.
How Custom Integrations Work [#how-custom-integrations-work]
When you create a shop platform in Openship, instead of setting function values to built-in slugs like "shopify", you can provide HTTP URLs. Openship will make POST requests to these endpoints with the required parameters and expect specific response formats.
For example:
* **Built-in**: `searchProductsFunction: "shopify"`
* **Custom**: `searchProductsFunction: "https://your-app.com/api/search-products"`
Required HTTP Endpoints [#required-http-endpoints]
Your custom shop integration must implement the following HTTP endpoints that match the function interface from the shop integration guide:
Product Search - `/api/search-products` [#product-search---apisearch-products]
**Purpose**: Openship uses this endpoint to allow users to search and browse products from your shop when creating product matches or exploring available inventory
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ------------- | ----------------------------------------- | -------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `searchEntry` | `string` | Search query |
| `after?` | `string` | Pagination cursor |
**Response Format**:
| Field | Type | Description |
| ---------- | --------------------------------------------- | ------------------------ |
| `products` | `Product[]` | Array of product objects |
| `pageInfo` | `{ hasNextPage: boolean; endCursor: string }` | Pagination information |
Implementation
Response
```typescript
// POST /api/search-products
export default async function handler(req, res) {
const { platform, searchEntry, after } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your custom product data (could come from database, API, etc.)
const allProducts = [
{
image: "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
title: "Wireless Bluetooth Headphones",
productId: "headphones001",
variantId: "black",
price: "1337.00",
availableForSale: true,
inventory: 42,
inventoryTracked: true,
productLink: "https://demo-store.com/products/bluetooth-headphones",
cursor: "eyJpZCI6ImdpYnNvbjAwMSJ9"
},
{
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=CHARGER",
title: "Portable Phone Charger Module",
productId: "charger001",
variantId: "black-box",
price: "2000.00",
availableForSale: true,
inventory: 5,
inventoryTracked: true,
productLink: "https://demo-store.com/products/phone-charger",
cursor: "eyJpZCI6InNldGVjMDAxIn0="
},
{
image: "https://via.placeholder.com/300x300/0000FF/FFFFFF?text=WAR",
title: "Smart Watch Series 5",
productId: "watch001",
variantId: "default",
price: "25000.00",
availableForSale: false,
inventory: 0,
inventoryTracked: true,
productLink: "https://demo-store.com/products/smart-watch",
cursor: "eyJpZCI6IndvcHIwMDEifQ=="
}
];
let filteredProducts = allProducts;
// Filter by search term if provided
if (searchEntry) {
filteredProducts = allProducts.filter(product =>
product.title.toLowerCase().includes(searchEntry.toLowerCase())
);
}
// Simple pagination (in real app, use proper pagination)
let startIndex = 0;
if (after) {
const decodedCursor = JSON.parse(Buffer.from(after, 'base64').toString());
startIndex = allProducts.findIndex(p => p.productId === decodedCursor.id) + 1;
}
const paginatedProducts = filteredProducts.slice(startIndex, startIndex + 10);
const hasNextPage = startIndex + 10 < filteredProducts.length;
const endCursor = paginatedProducts.length > 0
? paginatedProducts[paginatedProducts.length - 1].cursor
: null;
return res.status(200).json({
products: paginatedProducts,
pageInfo: {
hasNextPage,
endCursor
}
});
}
```
```json
{
"products": [
{
"image": "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
"title": "Wireless Bluetooth Headphones",
"productId": "headphones001",
"variantId": "black",
"price": "1337.00",
"availableForSale": true,
"inventory": 42,
"inventoryTracked": true,
"productLink": "https://demo-store.com/products/bluetooth-headphones",
"cursor": "eyJpZCI6ImdpYnNvbjAwMSJ9"
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "eyJpZCI6ImdpYnNvbjAwMSJ9"
}
}
```
Get Single Product - `/api/get-product` [#get-single-product---apiget-product]
**Purpose**: Openship uses this endpoint to fetch detailed product information when users select specific products for matching or viewing current inventory levels
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ------------ | ----------------------------------------- | -------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `productId` | `string` | Product identifier |
| `variantId?` | `string` | Variant identifier |
**Response Format**:
| Field | Type | Description |
| --------- | --------- | --------------------------------------- |
| `product` | `Product` | Single product object with full details |
Implementation
Response
```typescript
// POST /api/get-product
export default async function handler(req, res) {
const { platform, productId, variantId } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your product database (could be real database, API, etc.)
const products = {
"headphones001": {
black: {
image: "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
title: "Wireless Bluetooth Headphones - Midnight Black",
productId: "headphones001",
variantId: "black",
price: "1337.00",
availableForSale: true,
inventory: 42,
inventoryTracked: true,
productLink: "https://demo-store.com/products/bluetooth-headphones"
}
},
"charger001": {
"black-box": {
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=CHARGER",
title: "Portable Phone Charger Module - Black Box",
productId: "charger001",
variantId: "black-box",
price: "2000.00",
availableForSale: true,
inventory: 5,
inventoryTracked: true,
productLink: "https://demo-store.com/products/phone-charger"
}
}
};
const product = products[productId]?.[variantId || 'default'];
if (!product) {
return res.status(404).json({ error: "Product not found" });
}
return res.status(200).json({ product });
}
```
```json
{
"product": {
"image": "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
"title": "Wireless Bluetooth Headphones - Midnight Black",
"productId": "headphones001",
"variantId": "black",
"price": "1337.00",
"availableForSale": true,
"inventory": 42,
"inventoryTracked": true,
"productLink": "https://demo-store.com/products/gibson-terminal"
}
}
```
Search Orders - `/api/search-orders` [#search-orders---apisearch-orders]
**Purpose**: Openship uses this endpoint to retrieve orders from your shop, allowing the system to automatically process incoming orders and trigger corresponding purchases on matched channel platforms
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ------------- | ----------------------------------------- | -------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `searchEntry` | `string` | Search query |
| `after?` | `string` | Pagination cursor |
**Response Format**:
| Field | Type | Description |
| ---------- | --------------------------------------------- | ---------------------- |
| `orders` | `Order[]` | Array of order objects |
| `pageInfo` | `{ hasNextPage: boolean; endCursor: string }` | Pagination information |
Implementation
Response
```typescript
// POST /api/search-orders
export default async function handler(req, res) {
const { platform, searchEntry, after } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your orders data source
const allOrders = [
{
orderId: "order001",
orderName: "#ORDER-001",
link: "https://demo-store.com/orders/order001",
date: "09/15/1995",
firstName: "John",
lastName: "Smith",
streetAddress1: "123 Main Street",
streetAddress2: "Apt 404",
city: "New York",
state: "NY",
zip: "10001",
country: "United States",
email: "customer@demo-store.com",
fulfillmentStatus: "unfulfilled",
financialStatus: "paid",
totalPrice: "1337.00",
currency: "USD",
lineItems: [
{
lineItemId: "order001item001",
name: "Wireless Bluetooth Headphones",
quantity: 1,
image: "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
price: "1337.00",
variantId: "black",
productId: "headphones001"
}
],
cursor: "eyJpZCI6ImhhY2sxOTk1MDAxIn0="
},
{
orderId: "sneakers1992001",
orderName: "#ORDER-002",
link: "https://demo-store.com/orders/sneakers1992001",
date: "09/09/1992",
firstName: "Jane",
lastName: "Doe",
streetAddress1: "12 Maple Dr",
streetAddress2: "",
city: "San Francisco",
state: "CA",
zip: "94102",
country: "United States",
email: "cosmo@demo-store.com",
fulfillmentStatus: "unfulfilled",
financialStatus: "paid",
totalPrice: "2000.00",
currency: "USD",
lineItems: [
{
lineItemId: "charger001item001",
name: "Portable Phone Charger Module",
quantity: 1,
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=CHARGER",
price: "2000.00",
variantId: "black-box",
productId: "charger001"
}
],
cursor: "eyJpZCI6InNuZWFrZXJzMTk5MjAwMSJ9"
}
];
let filteredOrders = allOrders;
// Filter by search term if provided
if (searchEntry) {
filteredOrders = allOrders.filter(order =>
order.orderName.toLowerCase().includes(searchEntry.toLowerCase()) ||
order.firstName.toLowerCase().includes(searchEntry.toLowerCase()) ||
order.lastName.toLowerCase().includes(searchEntry.toLowerCase())
);
}
return res.status(200).json({
orders: filteredOrders,
pageInfo: {
hasNextPage: false,
endCursor: null
}
});
}
```
```json
{
"orders": [
{
"orderId": "order001",
"orderName": "#ORDER-001",
"link": "https://demo-store.com/orders/order001",
"date": "09/15/1995",
"firstName": "John",
"lastName": "Smith",
"streetAddress1": "123 Main Street",
"streetAddress2": "Apt 404",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "United States",
"email": "customer@demo-store.com",
"fulfillmentStatus": "unfulfilled",
"financialStatus": "paid",
"totalPrice": "1337.00",
"currency": "USD",
"lineItems": [
{
"lineItemId": "order001item001",
"name": "Wireless Bluetooth Headphones",
"quantity": 1,
"image": "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
"price": "1337.00",
"variantId": "black",
"productId": "headphones001"
}
],
"cursor": "eyJpZCI6ImhhY2sxOTk1MDAxIn0="
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": null
}
}
```
Update Product - `/api/update-product` [#update-product---apiupdate-product]
**Purpose**: Openship uses this endpoint to automatically sync inventory levels and pricing between matched products across different platforms
**HTTP Method**: `POST`
**Request Body**:
| Field | Type | Description |
| ------------ | ----------------------------------------- | -------------------- |
| `platform` | `{ domain: string; accessToken: string }` | Platform credentials |
| `productId` | `string` | Product identifier |
| `variantId` | `string` | Variant identifier |
| `inventory?` | `number` | New inventory count |
| `price?` | `string` | New price |
**Response Format**:
| Field | Type | Description |
| --------- | ---------- | ------------------------ |
| `success` | `boolean` | Whether update succeeded |
| `results` | `object[]` | Update operation results |
Implementation
Response
```typescript
// POST /api/update-product
export default async function handler(req, res) {
const { platform, productId, variantId, inventory, price } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Simulate updating your product database
const results = [];
if (price !== undefined) {
// Update price in your system
console.log(`Updating price for ${productId}:${variantId} to ${price}`);
results.push({
operation: "price_update",
productId,
variantId,
oldPrice: "1337.00", // You'd get this from your database
newPrice: price,
success: true
});
}
if (inventory !== undefined) {
// Update inventory in your system
console.log(`Updating inventory for ${productId}:${variantId} to ${inventory}`);
results.push({
operation: "inventory_update",
productId,
variantId,
oldInventory: 42, // You'd get this from your database
newInventory: inventory,
success: true
});
}
return res.status(200).json({
success: true,
results
});
}
```
```json
{
"success": true,
"results": [
{
"operation": "price_update",
"productId": "headphones001",
"variantId": "black",
"oldPrice": "1337.00",
"newPrice": "1400.00",
"success": true
},
{
"operation": "inventory_update",
"productId": "headphones001",
"variantId": "black",
"oldInventory": 42,
"newInventory": 38,
"success": true
}
]
}
```
Webhook Event Handlers [#webhook-event-handlers]
Your custom shop can also implement webhook handlers to notify Openship of events in real-time:
Order Creation Handler - `/api/webhook/order-created` [#order-creation-handler---apiwebhookorder-created]
**Purpose**: Openship uses this webhook to instantly receive and process new orders from your shop platform, automatically triggering the workflow to create corresponding purchases on matched channel platforms
Implementation
```typescript
// POST /api/webhook/order-created
export default async function handler(req, res) {
const { event, headers } = req.body;
// Verify webhook authenticity (implement your own verification)
if (!verifyWebhookSignature(headers)) {
return res.status(403).json({ error: "Invalid signature" });
}
// Transform your webhook event to Openship format
const order = {
id: event.order_id,
name: event.order_number,
email: event.customer_email,
financialStatus: event.payment_status,
fulfillmentStatus: "unfulfilled",
totalPrice: event.total_amount,
currency: event.currency,
lineItems: event.items.map(item => ({
id: item.line_item_id,
title: item.product_name,
quantity: item.quantity,
price: item.unit_price,
variantId: item.variant_id,
productId: item.product_id
})),
shippingAddress: {
firstName: event.shipping_address.first_name,
lastName: event.shipping_address.last_name,
address1: event.shipping_address.street,
city: event.shipping_address.city,
province: event.shipping_address.state,
country: event.shipping_address.country,
zip: event.shipping_address.postal_code
},
createdAt: event.created_at,
updatedAt: event.updated_at
};
return res.status(200).json({ order, type: "order_created" });
}
```
Testing Your Custom Shop [#testing-your-custom-shop]
We've built a demo shop integration right into this documentation that you can use to test the complete Openship workflow before implementing your own custom shop.
Step 1: Create a Demo Shop Platform [#step-1-create-a-demo-shop-platform]
1. In your Openship dashboard, navigate to **Platform > Shops**
2. Click **"Add Platform"**
3. In the **"Create Shop Platform"** dialog:
* Select **"Demo"** from the Platform dropdown
* The Name field will auto-fill with **"Demo Shop"**
* Click **"Create Platform"**
This creates a platform that points to our demo shop endpoints at `https://docs.openship.org/api/demo/shop/*`
Step 2: Connect Your Demo Shop [#step-2-connect-your-demo-shop]
1. After creating the platform, click **"Create Shop"**
2. In the **"Create Shop"** dialog:
* **Platform**: Select "Demo Shop Platform"
* **Name**: "Demo" (or any name you prefer)
* **Domain**: "local"
* **Access Token**: "demo\_token"
3. Click **"Create Shop"**
Step 3: Test the Demo Integration [#step-3-test-the-demo-integration]
Once connected, you can test the complete workflow:
* Browse products using our demo product catalog
* View sample orders in the system
* Test product matching between shops and channels
* Experience the full order fulfillment process
Step 4: Replace with Your Implementation [#step-4-replace-with-your-implementation]
When you're ready to use your custom shop:
1. Go to your Demo Shop Platform settings
2. Replace the demo URLs with your actual implementation:
```
# Replace these demo URLs:
https://docs.openship.org/api/demo/shop/search-products
https://docs.openship.org/api/demo/shop/get-product
https://docs.openship.org/api/demo/shop/search-orders
https://docs.openship.org/api/demo/shop/update-product
# ... and other endpoints
# With your URLs:
https://your-app.com/api/search-products
https://your-app.com/api/get-product
https://your-app.com/api/search-orders
https://your-app.com/api/update-product
# ... and so on
```
This approach lets you learn how Openship works with our demo data first, then seamlessly transition to your own implementation.
# Create Channel Integration
Overview [#overview]
Channels are fulfillment destinations where Openship can request a downstream purchase for matched order lines. Current source contains compiled Shopify and Openfront channel adapters; a new compiled adapter can follow those contracts.
Adapter source is not fulfillment proof. A new channel must enforce user ownership, isolate credentials, derive exact item/quantity/address inputs, use provider idempotency, authenticate callbacks, persist failures, and reconcile purchase, cancellation, and tracking state. UI examples naming Amazon or other providers later in this guide are implementation examples, not built-in adapters.
Shopify Channel Integration Reference [#shopify-channel-integration-reference]
The Shopify channel integration (`/features/integrations/channel/shopify.ts`) demonstrates all required functions for a complete channel integration. This guide will explain how each function works and how to implement it for your own channel integration. Each function below has 3 parts, what Openship sends to the function, how the function works, and what Openship expects to be returned.
Function Reference [#function-reference]
Product Search - searchProductsFunction [#product-search---searchproductsfunction]
**Purpose**: Openship uses this function to search for products available for purchase when setting up product matches, allowing users to find fulfillment options for their shop products
**Request Body**:
| Prop | Type | Default |
| ------------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `searchEntry` | `string` | - |
| `after?` | `string` | - |
**Response Format**:
| Field | Type | Description |
| ---------- | --------------------------------------------- | ------------------------------------ |
| `products` | `Product[]` | Array of purchasable product objects |
| `pageInfo` | `{ hasNextPage: boolean; endCursor: string }` | Pagination information |
Implementation
Response
```typescript
export async function searchProductsFunction({
platform,
searchEntry,
after
}: {
platform: { domain: string; accessToken: string };
searchEntry: string;
after?: string;
}) {
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const gqlQuery = gql`
query SearchProducts($query: String, $after: String) {
productVariants(first: 15, query: $query, after: $after) {
edges {
node {
id
availableForSale
image { originalSrc }
price
title
product {
id
handle
title
images(first: 1) {
edges {
node { originalSrc }
}
}
}
inventoryQuantity
inventoryPolicy
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const { productVariants } = await shopifyClient.request(gqlQuery, {
query: searchEntry,
after,
});
if (productVariants.edges.length < 1) {
throw new Error("No products found from Shopify channel");
}
const products = productVariants.edges.map(({ node, cursor }) => ({
image: node.image?.originalSrc || node.product.images.edges[0]?.node.originalSrc,
title: `${node.product.title} - ${node.title}`,
productId: node.product.id.split("/").pop(),
variantId: node.id.split("/").pop(),
price: node.price,
availableForSale: node.availableForSale,
inventory: node.inventoryQuantity,
inventoryTracked: node.inventoryPolicy !== "deny",
productLink: `https://${platform.domain}/products/${node.product.handle}`,
cursor,
}));
return {
products,
pageInfo: productVariants.pageInfo
};
}
```
```json
{
"products": [
{
"image": "https://cdn.shopify.com/...",
"title": "Product Name - Variant Name",
"productId": "123456789",
"variantId": "987654321",
"price": "29.99",
"availableForSale": true,
"inventory": 50,
"inventoryTracked": true,
"productLink": "https://store.myshopify.com/products/product-handle",
"cursor": "eyJsYXN0X2lkIjo..."
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "eyJsYXN0X2lkIjo..."
}
}
```
Get Single Product - getProductFunction [#get-single-product---getproductfunction]
**Purpose**: Openship uses this function to fetch detailed product information from channel platforms when creating product matches, ensuring accurate pricing and availability data for automated purchasing
**Request Body**:
| Prop | Type | Default |
| ------------ | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `productId` | `string` | - |
| `variantId?` | `string` | - |
**Response Format**:
| Field | Type | Description |
| --------- | --------- | ------------------------------------------- |
| `product` | `Product` | Single product object with purchase details |
Implementation
Response
```typescript
export async function getProductFunction({
platform,
productId,
variantId,
}: {
platform: { domain: string; accessToken: string };
productId: string;
variantId?: string;
}) {
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const gqlQuery = gql`
query GetProduct($variantId: ID!) {
productVariant(id: $variantId) {
id
availableForSale
image { originalSrc }
price
title
product {
id
handle
title
images(first: 1) {
edges {
node { originalSrc }
}
}
}
inventoryQuantity
inventoryPolicy
}
}
`;
const fullVariantId = `gid://shopify/ProductVariant/${variantId}`;
const { productVariant } = await shopifyClient.request(gqlQuery, {
variantId: fullVariantId,
});
if (!productVariant) {
throw new Error("Product not found from Shopify channel");
}
const product = {
image: productVariant.image?.originalSrc ||
productVariant.product.images.edges[0]?.node.originalSrc,
title: `${productVariant.product.title} - ${productVariant.title}`,
productId: productVariant.product.id.split("/").pop(),
variantId: productVariant.id.split("/").pop(),
price: productVariant.price,
availableForSale: productVariant.availableForSale,
inventory: productVariant.inventoryQuantity,
inventoryTracked: productVariant.inventoryPolicy !== "deny",
productLink: `https://${platform.domain}/products/${productVariant.product.handle}`,
};
return { product };
}
```
```json
{
"product": {
"image": "https://cdn.shopify.com/...",
"title": "Product Name - Variant Name",
"productId": "123456789",
"variantId": "987654321",
"price": "29.99",
"availableForSale": true,
"inventory": 50,
"inventoryTracked": true,
"productLink": "https://store.myshopify.com/products/product-handle"
}
}
```
Create Purchase - createPurchaseFunction [#create-purchase---createpurchasefunction]
**Purpose**: Openship uses this function to automatically create purchases on channel platforms when orders are received from connected shops, enabling automated order fulfillment workflow
**Request Body**:
| Prop | Type | Default |
| ----------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `cartItems` | `CartItem[]` | - |
| `shipping?` | `ShippingAddress` | - |
| `notes?` | `string` | - |
**Response Format**:
| Field | Type | Description |
| ------------- | ------------ | ---------------------------------- |
| `purchaseId` | `string` | Unique identifier for the purchase |
| `orderNumber` | `string` | Human-readable order number |
| `totalPrice` | `string` | Total cost of the purchase |
| `invoiceUrl` | `string` | URL to the purchase invoice |
| `lineItems` | `LineItem[]` | Items included in the purchase |
| `status` | `string` | Current purchase state |
Implementation
Response
```typescript
export async function createPurchaseFunction({
platform,
cartItems,
shipping,
notes,
}: {
platform: { domain: string; accessToken: string };
cartItems: Array<{
variantId: string;
quantity: number;
price?: string;
}>;
shipping?: {
firstName: string;
lastName: string;
address1: string;
address2?: string;
city: string;
province: string;
country: string;
zip: string;
phone?: string;
};
notes?: string;
}) {
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
// Create draft order
const mutation = gql`
mutation CreateDraftOrder($input: DraftOrderInput!) {
draftOrderCreate(input: $input) {
draftOrder {
id
name
invoiceUrl
totalPrice
lineItems(first: 50) {
edges {
node {
id
title
quantity
originalUnitPrice
variant {
id
title
product {
id
title
}
}
}
}
}
}
userErrors {
field
message
}
}
}
`;
const lineItems = cartItems.map(item => ({
variantId: `gid://shopify/ProductVariant/${item.variantId}`,
quantity: item.quantity,
originalUnitPrice: item.price,
}));
const input: any = {
lineItems,
note: notes,
};
if (shipping) {
input.shippingAddress = {
firstName: shipping.firstName,
lastName: shipping.lastName,
address1: shipping.address1,
address2: shipping.address2,
city: shipping.city,
province: shipping.province,
country: shipping.country,
zip: shipping.zip,
phone: shipping.phone,
};
}
const result = await shopifyClient.request(mutation, { input });
if (result.draftOrderCreate.userErrors.length > 0) {
throw new Error(`Failed to create purchase: ${result.draftOrderCreate.userErrors.map(e => e.message).join(', ')}`);
}
const draftOrder = result.draftOrderCreate.draftOrder;
// Complete the draft order
const completeMutation = gql`
mutation CompleteDraftOrder($id: ID!) {
draftOrderComplete(id: $id) {
draftOrder {
order {
id
name
totalPrice
lineItems(first: 50) {
edges {
node {
id
title
quantity
variant { id }
}
}
}
}
}
userErrors {
field
message
}
}
}
`;
const completeResult = await shopifyClient.request(completeMutation, {
id: draftOrder.id,
});
if (completeResult.draftOrderComplete.userErrors.length > 0) {
throw new Error(`Failed to complete purchase: ${completeResult.draftOrderComplete.userErrors.map(e => e.message).join(', ')}`);
}
const order = completeResult.draftOrderComplete.draftOrder.order;
return {
purchaseId: order.id.split("/").pop(),
orderNumber: order.name,
totalPrice: order.totalPrice,
invoiceUrl: draftOrder.invoiceUrl,
lineItems: order.lineItems.edges.map(({ node }) => ({
id: node.id.split("/").pop(),
title: node.title,
quantity: node.quantity,
variantId: node.variant.id.split("/").pop(),
})),
status: "pending",
};
}
```
```json
{
"purchaseId": "123456789",
"orderNumber": "#1001",
"totalPrice": "59.98",
"invoiceUrl": "https://store.myshopify.com/invoices/123",
"lineItems": [
{
"id": "987654321",
"title": "Product Name",
"quantity": 2,
"variantId": "123"
}
],
"status": "pending"
}
```
Create Webhook [#create-webhook]
**Purpose**: Openship uses this function to set up real-time webhooks that notify the system when purchases are fulfilled or cancelled on channel platforms, enabling automatic tracking updates to customers
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `endpoint` | `string` | - |
| `events` | `string[]` | - |
**Response Format**:
| Field | Type | Description |
| ---------- | ----------- | -------------------------------- |
| `webhooks` | `Webhook[]` | Array of created webhook objects |
Implementation
Response
```typescript
export async function createWebhookFunction({
platform,
endpoint,
events,
}: {
platform: { domain: string; accessToken: string };
endpoint: string;
events: string[];
}) {
const mapTopic = {
ORDER_CREATED: "ORDERS_CREATE",
ORDER_CANCELLED: "ORDERS_CANCELLED",
ORDER_CHARGEBACKED: "DISPUTES_CREATE",
TRACKING_CREATED: "FULFILLMENTS_CREATE",
};
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const webhooks = [];
for (const event of events) {
const shopifyTopic = mapTopic[event] || event;
const mutation = gql`
mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
webhookSubscription {
id
endpoint {
__typename
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
userErrors {
field
message
}
}
}
`;
const result = await shopifyClient.request(mutation, {
topic: shopifyTopic.toUpperCase(),
webhookSubscription: {
callbackUrl: endpoint,
format: "JSON",
},
});
webhooks.push(result.webhookSubscriptionCreate.webhookSubscription);
}
return { webhooks };
}
```
```json
{
"webhooks": [
{
"id": "gid://shopify/WebhookSubscription/123456789",
"endpoint": {
"callbackUrl": "https://your-app.com/webhook"
}
}
]
}
```
OAuth Integration [#oauth-integration]
If you have a Shopify app (or app on any platform), you can implement OAuth functions to allow users to install your app directly instead of manually retrieving access tokens. This provides a smoother user experience where users can authorize your integration through the standard app installation flow.
OAuth Authorization [#oauth-authorization]
**Purpose**: Openship uses this function to generate secure OAuth authorization URLs that allow users to safely connect their channel platforms to Openship for automated purchasing
**Request Body**:
| Prop | Type | Default |
| ------------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `callbackUrl` | `string` | - |
**Response Format**:
| Field | Type | Description |
| --------- | -------- | -------------------------------------------- |
| `authUrl` | `string` | OAuth authorization URL for user redirection |
Implementation
```typescript
export async function oAuthFunction({
platform,
callbackUrl,
}: {
platform: { domain: string; accessToken: string };
callbackUrl: string;
}) {
const scopes = "read_products,write_products,read_orders,write_orders,read_inventory,write_inventory";
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${process.env.SHOPIFY_APP_KEY}&scope=${scopes}&redirect_uri=${callbackUrl}&state=${Math.random().toString(36).substring(7)}`;
return { authUrl: shopifyAuthUrl };
}
```
OAuth Token Exchange [#oauth-token-exchange]
**Purpose**: Openship uses this function to complete the OAuth flow by exchanging temporary authorization codes for permanent access tokens, establishing a secure connection to the user's channel platform
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `code` | `string` | - |
| `shop` | `string` | - |
| `state` | `string` | - |
**Response Format**:
| Field | Type | Description |
| ------------- | -------- | ----------------------------------------- |
| `accessToken` | `string` | OAuth access token for API authentication |
| `domain` | `string` | Channel domain for API requests |
Implementation
```typescript
export async function oAuthCallbackFunction({
platform,
code,
shop,
state,
}: {
platform: { domain: string; accessToken: string };
code: string;
shop: string;
state: string;
}) {
const tokenUrl = `https://${shop}/admin/oauth/access_token`;
const response = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: process.env.SHOPIFY_APP_KEY,
client_secret: process.env.SHOPIFY_APP_SECRET,
code,
}),
});
if (!response.ok) {
throw new Error("Failed to exchange OAuth code for access token");
}
const { access_token } = await response.json();
return {
accessToken: access_token,
domain: shop,
};
}
```
Webhook Event Handlers [#webhook-event-handlers]
Channel integrations must implement webhook handlers to process real-time events from the platform.
Fulfillment Tracking Handler - createTrackingWebhookHandler [#fulfillment-tracking-handler---createtrackingwebhookhandler]
**Purpose**: Openship uses this webhook handler to receive tracking information when purchases are shipped from channel platforms, automatically forwarding tracking details to customers and updating order status
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `event` | `any` | - |
| `headers` | `Record` | - |
**Response Format**:
| Field | Type | Description |
| ------------- | ----------------------- | ---------------------------- |
| `fulfillment` | `Fulfillment` | Tracking/fulfillment details |
| `type` | `"fulfillment_created"` | Event type identifier |
Implementation
Response
```typescript
export async function createTrackingWebhookHandler({
platform,
event,
headers,
}: {
platform: { domain: string; accessToken: string };
event: any;
headers: Record;
}) {
// Verify webhook authenticity
const hmac = headers["x-shopify-hmac-sha256"];
if (!hmac) {
throw new Error("Missing webhook HMAC");
}
const fulfillment = {
id: event.id,
orderId: event.order_id,
status: event.status,
trackingCompany: event.tracking_company,
trackingNumber: event.tracking_number,
trackingUrl: event.tracking_url,
purchaseId: event.order_id?.toString(),
lineItems: event.line_items.map((item) => ({
id: item.id,
title: item.title,
quantity: item.quantity,
variantId: item.variant_id,
productId: item.product_id,
})),
createdAt: event.created_at,
updatedAt: event.updated_at,
};
return { fulfillment, type: "fulfillment_created" };
}
```
```json
{
"fulfillment": {
"id": 555666777,
"orderId": 123456789,
"status": "success",
"trackingCompany": "UPS",
"trackingNumber": "1Z999AA1234567890",
"trackingUrl": "https://wwwapps.ups.com/tracking/tracking.cgi?tracknum=1Z999AA1234567890",
"purchaseId": "123456789",
"lineItems": [
{
"id": 987654321,
"title": "Product Name",
"quantity": 2,
"variantId": "123",
"productId": "456"
}
],
"createdAt": "2025-01-15T12:00:00Z",
"updatedAt": "2025-01-15T12:00:00Z"
},
"type": "fulfillment_created"
}
```
Purchase Cancellation Handler - cancelPurchaseWebhookHandler [#purchase-cancellation-handler---cancelpurchasewebhookhandler]
**Purpose**: Openship uses this webhook handler to process purchase cancellation events from channel platforms, automatically handling refunds and updating order status across connected platforms
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `event` | `any` | - |
| `headers` | `Record` | - |
**Response Format**:
| Field | Type | Description |
| ------- | ---------------------- | --------------------------------------------------- |
| `order` | `Order` | Cancelled purchase object with cancellation details |
| `type` | `"purchase_cancelled"` | Event type identifier |
Implementation
Response
```typescript
export async function cancelPurchaseWebhookHandler({
platform,
event,
headers,
}: {
platform: { domain: string; accessToken: string };
event: any;
headers: Record;
}) {
// Verify webhook authenticity
const hmac = headers["x-shopify-hmac-sha256"];
if (!hmac) {
throw new Error("Missing webhook HMAC");
}
const order = {
id: event.id,
name: event.name,
cancelReason: event.cancel_reason,
cancelledAt: event.cancelled_at,
refund: event.refunds?.[0] || null,
lineItems: event.line_items.map((item) => ({
id: item.id,
title: item.title,
quantity: item.quantity,
variantId: item.variant_id,
productId: item.product_id,
})),
};
return { order, type: "purchase_cancelled" };
}
```
```json
{
"order": {
"id": 123456789,
"name": "#1001",
"cancelReason": "customer",
"cancelledAt": "2025-01-15T11:00:00Z",
"refund": {
"id": 111222333,
"amount": "59.98",
"currency": "USD"
},
"lineItems": [
{
"id": 987654321,
"title": "Product Name",
"quantity": 2,
"variantId": "123",
"productId": "456"
}
]
},
"type": "purchase_cancelled"
}
```
Adding Your Channel Integration to Openship [#adding-your-channel-integration-to-openship]
Once you've implemented your channel integration functions, you need to create a channel platform in Openship. Here's exactly what you'll see in the Openship interface:
1\. Navigate to Channel Platforms [#1-navigate-to-channel-platforms]
In Openship, go to the **Channels** page where you'll see the platform management interface:
Platforms
2\. Create Channel Platform Dialog [#2-create-channel-platform-dialog]
Click the **"Create platform to get started"** button to open the platform creation dialog:
Create Channel Platform
Create a platform based on an existing template
3\. Platform Template Selection [#3-platform-template-selection]
When you click the dropdown, you'll see the available channel templates:
Templates
Amazon
eBay
Etsy
Demo
Facebook
(soon)
Google
(soon)
Walmart
(soon)
Custom
Start from scratch...
4\. For Built-in Integration - Select Template [#4-for-built-in-integration---select-template]
If you created a built-in integration file (e.g., `myplatform.ts` in `/features/integrations/channel/`), you would:
1. **Add your template to the channelAdapters list** in `CreatePlatform.tsx`:
```typescript
const channelAdapters = {
amazon: "amazon",
ebay: "ebay",
etsy: "etsy",
demo: "demo", // Demo integration for testing
myplatform: "myplatform", // Add your integration here
// ...
};
```
2. **Select your template** from the dropdown
3. **Fill in the basic fields**: Name, App Key, App Secret (if needed)
5\. For Custom HTTP Integration - Select "Start from scratch..." [#5-for-custom-http-integration---select-start-from-scratch]
If you're using HTTP endpoints, select **"Start from scratch..."** to see all the function fields:
When you select **"Start from scratch..."**, you'll see form fields where you can enter your custom HTTP endpoints for each function.
6\. After Creating Platform [#6-after-creating-platform]
Once you create the platform, it appears in the platforms list:
Platforms
MY CHANNEL
Actions
7\. Create Channels Using Your Platform [#7-create-channels-using-your-platform]
Now you can click **"Create Channel"** to create individual channel instances that use your platform template. Each channel will use the same integration logic but with different credentials (domain, access tokens, etc.).
The platform system ensures your integration functions are reusable across multiple channel instances while maintaining clean separation between template logic and instance-specific configurations.
# Create Shop Integration
Overview [#overview]
Shops are source systems where customer orders originate. Current Openship source contains compiled Shopify and Openfront shop adapters; a new compiled adapter can follow those contracts.
Adapter source is not order-source proof. A new shop adapter must enforce user ownership, isolate credentials, validate bounded responses, authenticate callbacks, claim event IDs, preserve source order identity, and reconcile updates. UI examples naming WooCommerce or other providers later in this guide are implementation examples, not built-in adapters.
Shopify Shop Integration Reference [#shopify-shop-integration-reference]
The Shopify shop integration (`/features/integrations/shop/shopify.ts`) demonstrates all required functions for a complete shop integration. This guide will explain how each function works and how to implement it for your own shop integration. Each function below has 3 parts, what Openship sends to the function, how the function works, and what Openship expects to be returned.
Function Reference [#function-reference]
Product Search - searchProductsFunction [#product-search---searchproductsfunction]
**Purpose**: Openship uses this function to allow users to search and browse products from connected shop platforms when creating product matches or exploring available inventory
**Request Body**:
| Prop | Type | Default |
| ------------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `searchEntry` | `string` | - |
| `after?` | `string` | - |
**Response Format**:
| Field | Type | Description |
| ---------- | --------------------------------------------- | ------------------------ |
| `products` | `Product[]` | Array of product objects |
| `pageInfo` | `{ hasNextPage: boolean; endCursor: string }` | Pagination information |
Implementation
Response
```typescript
export async function searchProductsFunction({
platform,
searchEntry,
after
}: {
platform: { domain: string; accessToken: string };
searchEntry: string;
after?: string;
}) {
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const gqlQuery = gql`
query SearchProducts($query: String, $after: String) {
productVariants(first: 15, query: $query, after: $after) {
edges {
node {
id
availableForSale
image { originalSrc }
price
title
product {
id
handle
title
images(first: 1) {
edges {
node { originalSrc }
}
}
}
inventoryQuantity
inventoryPolicy
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const { productVariants } = await shopifyClient.request(gqlQuery, {
query: searchEntry,
after,
});
const products = productVariants.edges.map(({ node, cursor }) => ({
image: node.image?.originalSrc || node.product.images.edges[0]?.node.originalSrc,
title: `${node.product.title} - ${node.title}`,
productId: node.product.id.split("/").pop(),
variantId: node.id.split("/").pop(),
price: node.price,
availableForSale: node.availableForSale,
inventory: node.inventoryQuantity,
inventoryTracked: node.inventoryPolicy !== "deny",
productLink: `https://${platform.domain}/products/${node.product.handle}`,
cursor,
}));
return { products, pageInfo: productVariants.pageInfo };
}
```
```json
{
"products": [
{
"image": "https://cdn.shopify.com/...",
"title": "Product Name - Variant Name",
"productId": "123456789",
"variantId": "987654321",
"price": "29.99",
"availableForSale": true,
"inventory": 50,
"inventoryTracked": true,
"productLink": "https://store.myshopify.com/products/product-handle",
"cursor": "eyJsYXN0X2lkIjo..."
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "eyJsYXN0X2lkIjo..."
}
}
```
Get Single Product - getProductFunction [#get-single-product---getproductfunction]
**Purpose**: Openship uses this function to fetch detailed product information when users select specific products for matching, viewing current inventory levels, or syncing product data between platforms
**Request Body**:
| Prop | Type | Default |
| ------------ | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `productId` | `string` | - |
| `variantId?` | `string` | - |
**Response Format**:
| Field | Type | Description |
| --------- | --------- | --------------------------------------- |
| `product` | `Product` | Single product object with full details |
Implementation
Response
```typescript
export async function getProductFunction({
platform,
productId,
variantId,
}: {
platform: { domain: string; accessToken: string };
productId: string;
variantId?: string;
}) {
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const gqlQuery = gql`
query GetProduct($variantId: ID!) {
productVariant(id: $variantId) {
id
availableForSale
image { originalSrc }
price
title
product {
id
handle
title
images(first: 1) {
edges {
node { originalSrc }
}
}
}
inventoryQuantity
inventoryPolicy
}
}
`;
const fullVariantId = `gid://shopify/ProductVariant/${variantId}`;
const { productVariant } = await shopifyClient.request(gqlQuery, {
variantId: fullVariantId,
});
if (!productVariant) {
throw new Error("Product not found from Shopify");
}
const product = {
image: productVariant.image?.originalSrc ||
productVariant.product.images.edges[0]?.node.originalSrc,
title: `${productVariant.product.title} - ${productVariant.title}`,
productId: productVariant.product.id.split("/").pop(),
variantId: productVariant.id.split("/").pop(),
price: productVariant.price,
availableForSale: productVariant.availableForSale,
inventory: productVariant.inventoryQuantity,
inventoryTracked: productVariant.inventoryPolicy !== "deny",
productLink: `https://${platform.domain}/admin/products/${productVariant.product.id.split("/").pop()}/variants/${productVariant.id.split("/").pop()}`,
};
return { product };
}
```
```json
{
"product": {
"image": "https://cdn.shopify.com/...",
"title": "Product Name - Variant Name",
"productId": "123456789",
"variantId": "987654321",
"price": "29.99",
"availableForSale": true,
"inventory": 50,
"inventoryTracked": true,
"productLink": "https://store.myshopify.com/admin/products/123456789/variants/987654321"
}
}
```
Search Orders - searchOrdersFunction [#search-orders---searchordersfunction]
**Purpose**: Openship uses this function to retrieve orders from connected shop platforms, allowing the system to automatically process incoming orders and trigger corresponding purchases on matched channel platforms
**Request Body**:
| Prop | Type | Default |
| ------------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `searchEntry` | `string` | - |
| `after?` | `string` | - |
**Response Format**:
| Field | Type | Description |
| ---------- | --------------------------------------------- | ---------------------------------------------------------- |
| `orders` | `Order[]` | Array of order objects with customer and line item details |
| `pageInfo` | `{ hasNextPage: boolean; endCursor: string }` | Pagination information |
Implementation
Response
```typescript
export async function searchOrdersFunction({
platform,
searchEntry,
after,
}: {
platform: { domain: string; accessToken: string };
searchEntry: string;
after?: string;
}) {
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const gqlQuery = gql`
query SearchOrders($query: String, $after: String) {
orders(first: 15, query: $query, after: $after) {
edges {
node {
id
name
email
createdAt
updatedAt
displayFulfillmentStatus
displayFinancialStatus
totalPriceSet {
presentmentMoney {
amount
currencyCode
}
}
shippingAddress {
firstName
lastName
address1
address2
city
province
zip
country
}
lineItems(first: 10) {
edges {
node {
id
title
quantity
image { originalSrc }
variant {
id
title
price
product {
id
title
handle
}
}
}
}
}
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const { orders } = await shopifyClient.request(gqlQuery, {
query: searchEntry,
after,
});
const formattedOrders = orders.edges.map(({ node, cursor }) => ({
orderId: node.id.split("/").pop(),
orderName: node.name,
link: `https://${platform.domain}/admin/orders/${node.id.split("/").pop()}`,
date: new Date(node.createdAt).toLocaleDateString(),
firstName: node.shippingAddress?.firstName || "",
lastName: node.shippingAddress?.lastName || "",
streetAddress1: node.shippingAddress?.address1 || "",
streetAddress2: node.shippingAddress?.address2 || "",
city: node.shippingAddress?.city || "",
state: node.shippingAddress?.province || "",
zip: node.shippingAddress?.zip || "",
country: node.shippingAddress?.country || "",
email: node.email || "",
fulfillmentStatus: node.displayFulfillmentStatus,
financialStatus: node.displayFinancialStatus,
totalPrice: node.totalPriceSet.presentmentMoney.amount,
currency: node.totalPriceSet.presentmentMoney.currencyCode,
lineItems: node.lineItems.edges.map(({ node: lineItem }) => ({
lineItemId: lineItem.id.split("/").pop(),
name: lineItem.title,
quantity: lineItem.quantity,
image: lineItem.image?.originalSrc || "",
price: lineItem.variant?.price || "0",
variantId: lineItem.variant?.id.split("/").pop(),
productId: lineItem.variant?.product.id.split("/").pop(),
})),
cursor,
}));
return { orders: formattedOrders, pageInfo: orders.pageInfo };
}
```
```json
{
"orders": [
{
"orderId": "123456789",
"orderName": "#1001",
"link": "https://store.myshopify.com/admin/orders/123456789",
"date": "1/15/2025",
"firstName": "John",
"lastName": "Doe",
"streetAddress1": "123 Main St",
"streetAddress2": "Apt 1",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "United States",
"email": "john@example.com",
"fulfillmentStatus": "unfulfilled",
"financialStatus": "paid",
"totalPrice": "59.98",
"currency": "USD",
"lineItems": [
{
"lineItemId": "987654321",
"name": "Product Name",
"quantity": 2,
"image": "https://cdn.shopify.com/...",
"price": "29.99",
"variantId": "123",
"productId": "456"
}
],
"cursor": "eyJsYXN0X2lkIjo..."
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": null
}
}
```
Update Product - updateProductFunction [#update-product---updateproductfunction]
**Purpose**: Openship uses this function to automatically sync inventory levels and pricing between matched products across different platforms, ensuring accurate stock counts and competitive pricing
**Request Body**:
| Prop | Type | Default |
| ------------ | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `productId` | `string` | - |
| `variantId` | `string` | - |
| `inventory?` | `number` | - |
| `price?` | `string` | - |
**Response Format**:
| Field | Type | Description |
| --------- | ---------- | ------------------------------------------- |
| `success` | `boolean` | Whether the update operation succeeded |
| `results` | `object[]` | Array of mutation results from the platform |
Implementation
Response
```typescript
export async function updateProductFunction({
platform,
productId,
variantId,
inventory,
price,
}: {
platform: { domain: string; accessToken: string };
productId: string;
variantId: string;
inventory?: number;
price?: string;
}) {
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const mutations = [];
// Update price if provided
if (price !== undefined) {
const updatePriceMutation = gql`
mutation UpdateProductVariantPrice($input: ProductVariantInput!) {
productVariantUpdate(input: $input) {
productVariant {
id
price
}
userErrors {
field
message
}
}
}
`;
mutations.push(
shopifyClient.request(updatePriceMutation, {
input: {
id: `gid://shopify/ProductVariant/${variantId}`,
price: price,
},
})
);
}
// Update inventory if provided
if (inventory !== undefined) {
// Get inventory item ID and location
const getVariantQuery = gql`
query GetVariantWithInventory($id: ID!) {
productVariant(id: $id) {
inventoryQuantity
inventoryItem { id }
}
}
`;
const variantData = await shopifyClient.request(getVariantQuery, {
id: `gid://shopify/ProductVariant/${variantId}`,
});
if (!variantData.productVariant?.inventoryItem?.id) {
throw new Error("Unable to find inventory item for variant");
}
// Get first location
const getLocationsQuery = gql`
query GetLocations {
locations(first: 1) {
edges {
node {
id
name
}
}
}
}
`;
const locationsData = await shopifyClient.request(getLocationsQuery);
const location = locationsData.locations.edges[0]?.node;
if (!location) {
throw new Error("No locations found for shop");
}
// Update inventory
const updateInventoryMutation = gql`
mutation InventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) {
inventoryAdjustQuantities(input: $input) {
inventoryAdjustmentGroup { id }
userErrors {
field
message
}
}
}
`;
mutations.push(
shopifyClient.request(updateInventoryMutation, {
input: {
reason: "correction",
name: "available",
changes: [{
inventoryItemId: variantData.productVariant.inventoryItem.id,
locationId: location.id,
delta: inventory
}]
}
})
);
}
const results = await Promise.all(mutations);
return { success: true, results };
}
```
```json
{
"success": true,
"results": [
{
"productVariantUpdate": {
"productVariant": {
"id": "gid://shopify/ProductVariant/123",
"price": "29.99"
},
"userErrors": []
}
}
]
}
```
Create Webhook [#create-webhook]
**Purpose**: Openship uses this function to set up real-time webhooks that automatically notify the system when orders are created, cancelled, or fulfilled on your shop platform, enabling instant order processing and inventory updates
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `endpoint` | `string` | - |
| `events` | `string[]` | - |
**Response Format**:
| Field | Type | Description |
| ----------- | ----------- | -------------------------------- |
| `webhooks` | `Webhook[]` | Array of created webhook objects |
| `webhookId` | `string` | ID of the first created webhook |
Implementation
Response
```typescript
export async function createWebhookFunction({
platform,
endpoint,
events,
}: {
platform: { domain: string; accessToken: string };
endpoint: string;
events: string[];
}) {
const mapTopic = {
ORDER_CREATED: "ORDERS_CREATE",
ORDER_CANCELLED: "ORDERS_CANCELLED",
ORDER_CHARGEBACKED: "DISPUTES_CREATE",
TRACKING_CREATED: "FULFILLMENTS_CREATE",
};
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
headers: {
"X-Shopify-Access-Token": platform.accessToken,
},
}
);
const webhooks = [];
for (const event of events) {
const shopifyTopic = mapTopic[event] || event;
const mutation = gql`
mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
webhookSubscription {
id
endpoint {
__typename
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
userErrors {
field
message
}
}
}
`;
const result = await shopifyClient.request(mutation, {
topic: shopifyTopic,
webhookSubscription: {
callbackUrl: endpoint,
format: "JSON",
},
});
if (result.webhookSubscriptionCreate.userErrors.length > 0) {
throw new Error(`Error creating webhook: ${result.webhookSubscriptionCreate.userErrors[0].message}`);
}
webhooks.push(result.webhookSubscriptionCreate.webhookSubscription);
}
const webhookId = webhooks[0]?.id?.split("/").pop();
return { webhooks, webhookId };
}
```
```json
{
"webhooks": [
{
"id": "gid://shopify/WebhookSubscription/123456789",
"endpoint": {
"callbackUrl": "https://your-app.com/webhook"
}
}
],
"webhookId": "123456789"
}
```
OAuth Integration [#oauth-integration]
If you have a Shopify app (or app on any platform), you can implement OAuth functions to allow users to install your app directly instead of manually retrieving access tokens. This provides a smoother user experience where users can authorize your integration through the standard app installation flow.
OAuth Authorization [#oauth-authorization]
**Purpose**: Openship uses this function to generate secure OAuth authorization URLs that allow users to safely connect their shop platforms to Openship without sharing sensitive credentials
**Request Body**:
| Prop | Type | Default |
| ------------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `callbackUrl` | `string` | - |
**Response Format**:
| Field | Type | Description |
| --------- | -------- | -------------------------------------------- |
| `authUrl` | `string` | OAuth authorization URL for user redirection |
Implementation
```typescript
export async function oAuthFunction({
platform,
callbackUrl,
}: {
platform: { domain: string; accessToken: string };
callbackUrl: string;
}) {
const scopes = "read_products,write_products,read_orders,write_orders,read_inventory,write_inventory";
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${process.env.SHOPIFY_APP_KEY}&scope=${scopes}&redirect_uri=${callbackUrl}&state=${Math.random().toString(36).substring(7)}`;
return { authUrl: shopifyAuthUrl };
}
```
OAuth Token Exchange [#oauth-token-exchange]
**Purpose**: Openship uses this function to complete the OAuth flow by exchanging temporary authorization codes for permanent access tokens, establishing a secure connection to the user's shop platform
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `code` | `string` | - |
| `shop` | `string` | - |
| `state` | `string` | - |
**Response Format**:
| Field | Type | Description |
| ------------- | -------- | ----------------------------------------- |
| `accessToken` | `string` | OAuth access token for API authentication |
| `domain` | `string` | Shop domain for API requests |
Implementation
```typescript
export async function oAuthCallbackFunction({
platform,
code,
shop,
state,
}: {
platform: { domain: string; accessToken: string };
code: string;
shop: string;
state: string;
}) {
const tokenUrl = `https://${shop}/admin/oauth/access_token`;
const response = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: process.env.SHOPIFY_APP_KEY,
client_secret: process.env.SHOPIFY_APP_SECRET,
code,
}),
});
if (!response.ok) {
throw new Error("Failed to exchange OAuth code for access token");
}
const { access_token } = await response.json();
return {
accessToken: access_token,
domain: shop,
};
}
```
Webhook Event Handlers [#webhook-event-handlers]
Shop integrations must implement webhook handlers to process real-time events from the platform.
Order Creation Handler - createOrderWebhookHandler [#order-creation-handler---createorderwebhookhandler]
**Purpose**: Openship uses this webhook handler to instantly receive and process new orders from your shop platform, automatically triggering the workflow to create corresponding purchases on matched channel platforms
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `event` | `any` | - |
| `headers` | `Record` | - |
**Response Format**:
| Field | Type | Description |
| ------- | ----------------- | ---------------------- |
| `order` | `Order` | Processed order object |
| `type` | `"order_created"` | Event type identifier |
Implementation
Response
```typescript
export async function createOrderWebhookHandler({
platform,
event,
headers,
}: {
platform: { domain: string; accessToken: string };
event: any;
headers: Record;
}) {
// Verify webhook authenticity
const hmac = headers["x-shopify-hmac-sha256"];
if (!hmac) {
throw new Error("Missing webhook HMAC");
}
const order = {
id: event.id,
name: event.name,
email: event.email,
financialStatus: event.financial_status,
fulfillmentStatus: event.fulfillment_status,
totalPrice: event.total_price,
currency: event.currency,
lineItems: event.line_items.map((item) => ({
id: item.id,
title: item.title,
quantity: item.quantity,
price: item.price,
variantId: item.variant_id,
productId: item.product_id,
})),
shippingAddress: {
firstName: event.shipping_address?.first_name || "",
lastName: event.shipping_address?.last_name || "",
address1: event.shipping_address?.address1 || "",
address2: event.shipping_address?.address2 || "",
city: event.shipping_address?.city || "",
province: event.shipping_address?.province || "",
country: event.shipping_address?.country || "",
zip: event.shipping_address?.zip || "",
},
createdAt: event.created_at,
updatedAt: event.updated_at,
};
return { order, type: "order_created" };
}
```
```json
{
"order": {
"id": 123456789,
"name": "#1001",
"email": "customer@example.com",
"financialStatus": "paid",
"fulfillmentStatus": "unfulfilled",
"totalPrice": "59.98",
"currency": "USD",
"lineItems": [
{
"id": 987654321,
"title": "Product Name",
"quantity": 2,
"price": "29.99",
"variantId": "123",
"productId": "456"
}
],
"shippingAddress": {
"firstName": "John",
"lastName": "Doe",
"address1": "123 Main St",
"address2": "",
"city": "New York",
"province": "NY",
"country": "United States",
"zip": "10001"
},
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-15T10:00:00Z"
},
"type": "order_created"
}
```
Order Cancellation Handler - cancelOrderWebhookHandler [#order-cancellation-handler---cancelorderwebhookhandler]
**Purpose**: Openship uses this webhook handler to process order cancellation events from your shop platform, automatically handling refunds and updating inventory across connected platforms
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `event` | `any` | - |
| `headers` | `Record` | - |
**Response Format**:
| Field | Type | Description |
| ------- | ------------------- | ------------------------------------------------ |
| `order` | `Order` | Cancelled order object with cancellation details |
| `type` | `"order_cancelled"` | Event type identifier |
Implementation
Response
```typescript
export async function cancelOrderWebhookHandler({
platform,
event,
headers,
}: {
platform: { domain: string; accessToken: string };
event: any;
headers: Record;
}) {
// Verify webhook authenticity
const hmac = headers["x-shopify-hmac-sha256"];
if (!hmac) {
throw new Error("Missing webhook HMAC");
}
const order = {
id: event.id,
name: event.name,
cancelReason: event.cancel_reason,
cancelledAt: event.cancelled_at,
refund: event.refunds?.[0] || null,
lineItems: event.line_items.map((item) => ({
id: item.id,
title: item.title,
quantity: item.quantity,
variantId: item.variant_id,
productId: item.product_id,
})),
};
return { order, type: "order_cancelled" };
}
```
```json
{
"order": {
"id": 123456789,
"name": "#1001",
"cancelReason": "customer",
"cancelledAt": "2025-01-15T11:00:00Z",
"refund": {
"id": 111222333,
"amount": "59.98",
"currency": "USD"
},
"lineItems": [
{
"id": 987654321,
"title": "Product Name",
"quantity": 2,
"variantId": "123",
"productId": "456"
}
]
},
"type": "order_cancelled"
}
```
Tracking/Fulfillment Handler - addTrackingFunction [#trackingfulfillment-handler---addtrackingfunction]
**Purpose**: Openship uses this function to add tracking information to fulfilled orders, automatically updating customers with shipping details and tracking numbers from your connected platforms
**Request Body**:
| Prop | Type | Default |
| ---------- | ----------------------------------------- | ------- |
| `platform` | `{ domain: string; accessToken: string }` | - |
| `event` | `any` | - |
| `headers` | `Record` | - |
**Response Format**:
| Field | Type | Description |
| ------------- | ----------------------- | ---------------------------- |
| `fulfillment` | `Fulfillment` | Tracking/fulfillment details |
| `type` | `"fulfillment_created"` | Event type identifier |
Implementation
Response
```typescript
export async function addTrackingFunction({
platform,
event,
headers,
}: {
platform: { domain: string; accessToken: string };
event: any;
headers: Record;
}) {
// Verify webhook authenticity
const hmac = headers["x-shopify-hmac-sha256"];
if (!hmac) {
throw new Error("Missing webhook HMAC");
}
const fulfillment = {
id: event.id,
orderId: event.order_id,
status: event.status,
trackingCompany: event.tracking_company,
trackingNumber: event.tracking_number,
trackingUrl: event.tracking_url,
lineItems: event.line_items.map((item) => ({
id: item.id,
title: item.title,
quantity: item.quantity,
variantId: item.variant_id,
productId: item.product_id,
})),
createdAt: event.created_at,
updatedAt: event.updated_at,
};
return { fulfillment, type: "fulfillment_created" };
}
```
```json
{
"fulfillment": {
"id": 555666777,
"orderId": 123456789,
"status": "success",
"trackingCompany": "UPS",
"trackingNumber": "1Z999AA1234567890",
"trackingUrl": "https://wwwapps.ups.com/tracking/tracking.cgi?tracknum=1Z999AA1234567890",
"lineItems": [
{
"id": 987654321,
"title": "Product Name",
"quantity": 2,
"variantId": "123",
"productId": "456"
}
],
"createdAt": "2025-01-15T12:00:00Z",
"updatedAt": "2025-01-15T12:00:00Z"
},
"type": "fulfillment_created"
}
```
Adding Your Shop Integration to Openship [#adding-your-shop-integration-to-openship]
Once you've implemented your shop integration functions, you need to create a shop platform in Openship. Here's exactly what you'll see in the Openship interface:
1\. Navigate to Shop Platforms [#1-navigate-to-shop-platforms]
In Openship, go to the **Shops** page where you'll see the platform management interface:
Platforms
2\. Create Shop Platform Dialog [#2-create-shop-platform-dialog]
Click the **"Create platform to get started"** button to open the platform creation dialog:
Create Shop Platform
Create a platform based on an existing template
3\. Platform Template Selection [#3-platform-template-selection]
When you click the dropdown, you'll see the available templates:
Templates
Shopify
Bigcommerce
Woocommerce
Demo
Medusa
(soon)
Magento
(soon)
Stripe
(soon)
Custom
Start from scratch...
4\. For Built-in Integration - Select Template [#4-for-built-in-integration---select-template]
If you created a built-in integration file (e.g., `myplatform.ts` in `/features/integrations/shop/`), you would:
1. **Add your template to the shopAdapters list** in `CreatePlatform.tsx`:
```typescript
const shopAdapters = {
shopify: "shopify",
bigcommerce: "bigcommerce",
woocommerce: "woocommerce",
demo: "demo", // Demo integration for testing
myplatform: "myplatform", // Add your integration here
// ...
};
```
2. **Select your template** from the dropdown
3. **Fill in the basic fields**: Name, App Key, App Secret (if needed)
5\. For Custom HTTP Integration - Select "Start from scratch..." [#5-for-custom-http-integration---select-start-from-scratch]
If you're using HTTP endpoints, select **"Start from scratch..."** to see all the function fields:
When you select **"Start from scratch..."**, you'll see form fields where you can enter your custom HTTP endpoints for each function.
6\. After Creating Platform [#6-after-creating-platform]
Once you create the platform, it appears in the platforms list:
Platforms
MY PLATFORM
Actions
7\. Create Shops Using Your Platform [#7-create-shops-using-your-platform]
Now you can click **"Create Shop"** to create individual shop instances that use your platform template. Each shop will use the same integration logic but with different credentials (domain, access tokens, etc.).
The platform system ensures your integration functions are reusable across multiple shop instances while maintaining clean separation between template logic and instance-specific configurations.
# Implement a payment adapter
Use `features/keystone/utils/paymentProviderAdapter.ts` as the dispatch contract and the built-in `manual`, `paypal`, and `stripe` modules as local examples. A payment app can live outside Openfront: store its HTTPS operation routes on the `PaymentProvider` record instead of importing its code into the Openfront repository.
Operations [#operations]
Current dispatch covers:
```ts
createPaymentFunction({ provider, cart, amount, currency })
capturePaymentFunction({ provider, paymentId, amount })
refundPaymentFunction({ provider, paymentId, amount })
getPaymentStatusFunction({ provider, paymentId })
generatePaymentLinkFunction({ provider, paymentId })
handleWebhookFunction({ provider, event, headers })
```
A local module exports these functions. An HTTP adapter route receives `POST` JSON containing `provider` plus the listed operation arguments and returns the corresponding result as JSON.
An adapter may omit an operation it cannot support, but every caller and UI must fail explicitly rather than fabricate success.
Implementation rules [#implementation-rules]
* derive amount, currency, cart/order identity, and allowed transition from persisted Openfront state;
* use integer minor-unit amounts while honoring currencies without fractional units;
* give every create, capture, and refund intent an idempotency key and reconciliation path;
* return provider-neutral IDs, status, amount, and only the browser handoff fields required by the caller;
* never return raw credentials or unrestricted provider payloads to the browser;
* authenticate Openfront-to-adapter HTTP requests and protect against replay;
* verify webhook authenticity using the exact raw-body protocol required by the provider;
* validate the adapter's runtime JSON before changing Openfront state;
* separate authorization, capture, refund, cancellation, and unknown outcomes.
End-to-end boundary [#end-to-end-boundary]
Generic route dispatch does not by itself make checkout provider-neutral. In the current source, `completeActiveCart.ts` and invoice completion contain hard-coded provider-code switches. Before calling a custom adapter complete, replace those switches with a bounded completion contract that invokes the configured adapter operation, verifies the current amount/currency and selected session, handles idempotency, and reconciles uncertain provider outcomes.
Verification [#verification]
* unit-test every request/response mapping;
* contract-test each HTTP operation route with malformed and delayed responses;
* run provider sandbox initiation, capture, refund, status, link, and webhook scenarios;
* prove duplicate calls have one provider effect;
* prove wrong-cart, wrong-user, stale-session, and altered-amount calls fail;
* verify endpoint and credential changes require privileged operator authority;
* run complete checkout and invoice workflows against isolated PostgreSQL.
See [Add a payment adapter](/docs/openfront/ecommerce/how-to-guides/custom-payment-provider) for provider configuration and current limitations.
# Implement a shipping adapter
Use `features/keystone/utils/shippingProviderAdapter.ts` as the dispatch contract. The built-in `manual.ts`, `shippo.ts`, and `shipengine.ts` files are local examples; an independently deployed app can implement the same operations through URLs stored on the `ShippingProvider` record.
Operations [#operations]
```ts
getRatesFunction({ provider, order, dimensions })
validateAddressFunction({ provider, address })
createLabelFunction({ provider, order, rateId, dimensions, lineItems })
trackShipmentFunction({ provider, trackingNumber })
cancelLabelFunction({ provider, labelId })
```
For an HTTP adapter, Openfront sends `POST` JSON containing `provider` plus the operation arguments. The route app translates that payload to its carrier, aggregator, warehouse, local courier, or custom fulfillment API and returns the provider-neutral result expected by Openfront.
Provider-neutral results [#provider-neutral-results]
Return only the data needed by the domain caller:
* rates: stable rate ID, service, carrier, price, currency, and delivery estimate;
* validation: validity, normalized/suggested address, and safe errors;
* labels: status, label/tracking identifiers and URLs, carrier, service, rate, and bounded metadata;
* tracking: status, estimate, tracking URL, and normalized events;
* cancellation: provider result and final cancellation state.
Validate these runtime responses before creating or updating fulfillment records.
Rules [#rules]
* check operator authority, order ownership/scope, unfulfilled quantities, address, parcel, and rate eligibility before calling the adapter;
* authenticate calls from Openfront to an external route app;
* forward only the provider and customer/order fields required by that operation;
* treat label purchase and cancellation as idempotent intents;
* use timeouts and reconcile an uncertain label purchase before retrying;
* redact access tokens, addresses, labels, and provider payloads from logs and browser errors;
* deduplicate tracking callbacks or polling observations;
* keep fulfillment lifecycle and audit evidence in Openfront rather than the adapter app.
Current configuration gap [#current-configuration-gap]
The backend supports HTTP operation fields, but the current custom-provider forms do not wire them correctly: one stores `metadata.apiUrl`, and another selects a nonexistent local `custom` module. Configure the operation URLs through a trusted administrative path until those forms are repaired and covered by an end-to-end test.
Verification [#verification]
* unit-test request/response translation;
* contract-test malformed, delayed, unauthorized, and replayed route calls;
* run sandbox rates, validation, label, tracking, and cancellation;
* retry one label intent and assert one purchase;
* reject rate/address/order/line mismatches;
* verify endpoint and credential edits require privileged operator authority;
* reconcile label cost, state, tracking, and cancellation with the fulfillment record.
See [Add a shipping adapter](/docs/openfront/ecommerce/how-to-guides/custom-shipping-provider) for configuration details.
# Add a payment adapter
Openfront's payment adapter dispatcher supports two provider modes:
* a local adapter module such as `stripe`, `paypal`, or `manual` under `features/integrations/payment`;
* an external HTTP route app, selected by storing an HTTP endpoint in each operation field on the `PaymentProvider` record.
This database configuration is intentional: a merchant can connect or replace an external payment app without adding that app's implementation to the Openfront repository.
How dispatch works [#how-dispatch-works]
`features/keystone/utils/paymentProviderAdapter.ts` reads the configured field for the requested operation:
* `createPaymentFunction`
* `capturePaymentFunction`
* `refundPaymentFunction`
* `getPaymentStatusFunction`
* `generatePaymentLinkFunction`
* `handleWebhookFunction`
If the value starts with `http`, Openfront sends a JSON `POST` to that URL. Otherwise, it imports `features/integrations/payment/.ts` and invokes the export named by the operation field.
For example, payment creation sends the external route a body shaped like:
```json
{
"provider": "the configured provider record",
"cart": "the current cart projection",
"amount": 2500,
"currency": "USD"
}
```
Other routes receive the same `provider` object plus operation-specific values such as `paymentId`, `amount`, `event`, or `headers`. The route's JSON response must match the provider-neutral result expected by the Openfront caller.
Configure an external route app [#configure-an-external-route-app]
1. Deploy one trusted HTTPS route per operation, or deliberately route several operation fields to one app that can distinguish the request shape.
2. Create a `PaymentProvider` record with its name, `pp_…` code, enabled state, credentials/metadata, regions, and operation URLs.
3. Implement only the operations the payment app actually supports, while ensuring unsupported calls fail explicitly.
4. Test the provider against a disposable Openfront host and provider sandbox.
5. Verify payment initiation, browser handoff, capture, refund, status, payment-link, webhook, timeout, and reconciliation behavior required by the intended checkout.
The current HTTP bridge trusts database-configured destinations and sends the selected provider object plus operation data. Treat provider configuration as privileged executable integration configuration. Use trusted HTTPS destinations, tightly restrict who can change them, authenticate Openfront to the adapter app, minimize forwarded credentials/data, validate responses, add timeouts, and apply an outbound-network policy appropriate to the deployment. These are hardening requirements for the route-adapter design—not a reason to remove it.
Current source limitations [#current-source-limitations]
The generic dispatcher supports external payment routes, but the complete Ecommerce flow is not yet provider-neutral:
* the dashboard creation UI exposes built-in presets rather than a complete external-route form;
* `completeActiveCart.ts` and invoice completion still switch on the built-in Stripe, PayPal, and manual provider codes instead of completing through the configured adapter contract;
* webhook ingress and signature handling must be verified against each route app's raw-body requirements;
* the bridge currently has no built-in timeout, destination policy, adapter authentication, or runtime response schema.
Therefore a custom external adapter can participate in generic adapter calls, but a complete checkout cannot be claimed until the target revision removes the hard-coded completion switch and passes end-to-end tests.
Required verification [#required-verification]
* unauthorized users cannot create or alter provider routes or credentials;
* each operation reaches only its configured adapter destination;
* the adapter authenticates the Openfront caller and rejects replay where relevant;
* amounts and currency come from persisted server state;
* duplicate create/capture/refund requests have one provider effect;
* malformed, oversized, delayed, or partial adapter responses fail closed;
* provider credentials and payment data do not appear in logs or client responses;
* invalid and replayed webhooks make no state change;
* uncertain responses reconcile before another payment action.
See [Payment providers](/docs/openfront/ecommerce/payment-providers).
# Add a shipping adapter
Openfront's shipping adapter dispatcher supports two provider modes:
* a local module such as `shippo`, `shipengine`, or `manual` under `features/integrations/shipping`;
* an external HTTP route app selected by the operation URLs stored on a `ShippingProvider` record.
The route-app mode lets a merchant connect custom shipping logic without adding that implementation to the Openfront repository.
Adapter operations [#adapter-operations]
The configurable fields are:
* `getRatesFunction`
* `validateAddressFunction`
* `createLabelFunction`
* `trackShipmentFunction`
* `cancelLabelFunction`
`features/keystone/utils/shippingProviderAdapter.ts` reads the relevant field. If it starts with `http`, Openfront sends the route a JSON `POST` containing the selected `provider` object and the operation arguments. Otherwise, it imports `features/integrations/shipping/.ts` and calls the corresponding export.
Examples of operation arguments include:
* rates: `order` and `dimensions`;
* address validation: `address`;
* label creation: `order`, `rateId`, `dimensions`, and `lineItems`;
* tracking: `trackingNumber`;
* cancellation: `labelId`.
The external route returns the same provider-neutral JSON shape expected from a local module.
Configure an external route app [#configure-an-external-route-app]
1. Deploy trusted HTTPS endpoints for the operations the app supports.
2. Create a `ShippingProvider` with its name, active state, access token, from-address, regions, metadata, and operation URLs.
3. Have the route app translate Openfront's operation payload into the external service's API and normalize the result back into Openfront's rate, validation, label, tracking, or cancellation shape.
4. Test every enabled operation against synthetic addresses, disposable orders, and sandbox credentials.
5. Verify duplicate, timeout, provider-failure, and reconciliation behavior before buying real labels.
The current bridge sends the selected provider object—including its queried access token—and order, address, parcel, or tracking data to the configured destination. Provider routing is therefore privileged integration configuration. Restrict who can edit it, use trusted HTTPS routes, authenticate requests to the adapter app, minimize the payload, validate responses, add timeouts, and enforce the deployment's outbound-network policy. Preserve external route adapters; harden their trust boundary.
Current source limitations [#current-source-limitations]
The backend dispatcher and `ShippingProvider` model support operation URLs, but the dashboard paths are inconsistent:
* the order-level “new provider” form collects an API URL but stores it only in `metadata.apiUrl` instead of the five operation fields;
* the general custom-provider drawer stores `custom` as a local module token even though no `features/integrations/shipping/custom.ts` module exists;
* the HTTP bridge has no built-in timeout, destination validation, adapter authentication, or runtime response schema;
* separate operations do not include an explicit operation name in the POST body, so a shared endpoint must infer it from the payload unless the protocol is extended.
Configure the operation fields directly through a trusted administrative path until those UI paths are repaired. Do not treat successful provider-row creation as proof that route dispatch works.
Required verification [#required-verification]
* unauthorized users cannot read the access token or alter adapter destinations;
* operation URLs cannot be changed through untrusted storefront input;
* the adapter authenticates the Openfront caller;
* rate, address, order, and line-item relationships are checked before provider work;
* label purchase and cancellation are idempotent;
* malformed, delayed, partial, or oversized responses fail closed;
* adapter credentials and customer addresses are redacted from logs and client responses;
* tracking replay is deduplicated;
* label cost, status, tracking, and cancellation reconcile with the fulfillment record.
See [Shipping providers](/docs/openfront/ecommerce/shipping-providers).
# External dashboards
To create a custom dashboard, copy the skill and give it to your LLM. It will first ask which Openfront product and codebase you are using, whether you want to adapt the built-in dashboard or build a separate client, what you want to preserve, and what you want to change.
The skill tells the LLM to inspect that product's built-in dashboard, feature slices, GraphQL schema, operations, access rules, and authentication boundary before proposing work. Tell it what feels wrong in the existing dashboard, which operator workflows you need, who will use them, and any visual or interaction direction you already have.
# Dashboard overview
The dashboard is the authenticated operator surface for Openfront Ecommerce. App Router pages delegate to feature slices under `features/platform`; Keystone models and domain mutations remain in `features/keystone`.
A visible dashboard control does not prove its backend transition, provider call, or authorization. Test the exact action, wrong-role case, retry, and failure state before relying on it.
Current route groups [#current-route-groups]
* Catalog: products, product categories and collections.
* Orders: list, detail, creation and fulfillment pages.
* Markets: regions, countries, currencies, shipping options and store settings.
* Inventory and fulfillment: inventory, shipping and shipping-provider pages.
* Commerce: payment providers, discounts, gift cards, claims, price lists and invoices.
* Access and extension: users, API keys, apps/OAuth, business-account requests and system settings.
* Reporting: an analytics route and dashboard summary components.
Generic Keystone list pages also expose models permitted by the current role. Those pages are useful for administration, but sensitive lifecycle changes should still use a named operation rather than unrestricted field edits.
Setup order [#setup-order]
1. Create the first permitted dashboard user.
2. Run the [onboarding flow](/docs/openfront/ecommerce/getting-started) against an isolated database.
3. Verify store, region, currency, country and price relationships.
4. Review products and inventory.
5. Configure only providers you can test end to end.
6. Exercise cart -> checkout -> order -> fulfillment/refund with synthetic data.
Customization [#customization]
Change screens inside the relevant `features/platform/` slice and keep route pages thin. If you split the dashboard into another application, preserve session or bearer-token scope, CSRF/origin policy, tenant filters and narrow lifecycle mutations.
# External storefronts
To create a custom storefront, copy the skill and give it to your LLM. It will first ask which Openfront product and codebase you are using, whether you want to adapt the built-in storefront or build a separate client, what you dislike about the current storefront, what should stay, and what the replacement should feel like.
The skill tells the LLM to inspect that product's built-in storefront, routes, feature slices, GraphQL calls, generated schema, access rules, session behavior, and provider boundaries before proposing work. Tell it the customer journeys you need, the parts of the built-in experience you want changed, and any design, content, device, accessibility, or deployment requirements.
# Storefront overview
The built-in storefront lives under `app/(storefront)/[countryCode]`. It reads region-aware catalog data and provides product discovery, cart, checkout, confirmation and customer-account routes.
Current routes [#current-routes]
* regional home and store pages;
* product, category and collection detail;
* cart and checkout;
* order confirmation;
* sign-in and checkout-link entry;
* account profile, addresses, orders, invoices and invoicing.
Storefront UI and data helpers live under `features/storefront`. Product, cart, checkout, payment and order truth remains in the Keystone/domain layer.
Current storefront workflow [#current-storefront-workflow]
1. Resolve a country code and region.
2. Browse products, variants and prices for that region.
3. Create or resume a cart whose ID is stored in the storefront cookie.
4. Add or update lines and collect address, shipping and payment choices.
5. Initiate a payment session for an installed provider.
6. Call `completeActiveCart` to capture or record payment and create the order.
7. Load the resulting order for confirmation or customer history.
Current Ecommerce source does not consistently establish this as an ownership-safe, idempotent checkout boundary. `activeCart`, `updateActiveCart`, line-item updates, payment-session initiation and completion accept caller-supplied IDs and use privileged Keystone access without complete cart-owner, parent-line, replay or transaction checks. Treat the built-in flow as implementation source to harden, not as proof that an external client can safely expose those operations.
Before accepting live checkout, derive or validate totals on the server, bind every line and payment session to the authorized cart, serialize inventory changes, add an idempotency boundary, verify signed webhook replay handling, reconcile provider state, and test wrong-user and replay cases.
Custom storefronts [#custom-storefronts]
You can change the built-in storefront or build another client. Use narrow public/catalog operations and ownership-scoped cart/order operations. Do not expose private Keystone lists, provider credentials, internal costs or unrestricted status updates to make a separate client easier.
# Kitchen display system
The KDS is where the restaurant order stops being theoretical and starts becoming service.
Openfront Restaurant already routes tickets by kitchen station, tracks ticket age, supports all-day views, and lets staff complete items individually instead of treating the whole order like a single block.
What the KDS does today [#what-the-kds-does-today]
* groups tickets by kitchen station
* filters by active status
* separates prep and expediter lanes
* highlights urgent tickets
* tracks overdue and critical timing thresholds
* supports both ticket view and all-day view
* shows station throughput cards
* lets staff mark individual items fulfilled
* prevents expo from bumping a ticket if prep stations are still working
Timing rules in the current UI [#timing-rules-in-the-current-ui]
The KDS uses two simple age thresholds out of the box:
* **Warn** at 12 minutes
* **Critical** at 20 minutes
That gives the screen enough tension to be useful during service without turning it into a wall of alerts all the time.
How tickets get into the KDS [#how-tickets-get-into-the-kds]
Orders are grouped by station [#orders-are-grouped-by-station]
Each `OrderItem` points at a kitchen station through the menu item. During sync, items are grouped by station and turned into `KitchenTicket` records.
The queue is sorted for actual service pressure [#the-queue-is-sorted-for-actual-service-pressure]
Urgent orders float up. On-hold work drops down. Ticket priority and age break ties after that.
Cooks work at the item level [#cooks-work-at-the-item-level]
A cook can mark individual items fulfilled. Once every item on a ticket is fulfilled, the ticket moves to ready.
Expo controls the final handoff [#expo-controls-the-final-handoff]
If a ticket belongs to an expediter lane, the system checks whether prep stations are finished before it can be bumped to served.
Views that already exist [#views-that-already-exist]
Ticket view [#ticket-view]
This is the default queue view. It is best when the kitchen is working order by order.
All-day view [#all-day-view]
All-day view rolls active demand up by item. It is useful when the line needs a fast answer to a question like, "How many fries are we actually short on right now?"
Station metrics [#station-metrics]
The KDS also shows simple throughput cards by station, including active tickets, ready tickets, overdue work, and average age.
Current implementation notes [#current-implementation-notes]
The KDS is fed by ticket sync logic in `features/keystone/mutations/kdsTickets.ts`. It already does a good job of reconciling orders into station tickets and keeping state consistent.
What it is not trying to be yet is a giant custom kitchen-ops platform with device provisioning, printer fallbacks, and every enterprise edge case built in. The important restaurant concepts are already represented.
The current KDS feels most at home in kitchens that want clean digital ticket flow, station routing, and better timing visibility without carrying the weight of a legacy POS stack.
# Menu management
The menu layer is one of the stronger parts of Openfront Restaurant already. The data model is flexible enough for a real restaurant menu, and the admin screens make that model usable instead of hiding it behind raw CRUD.
What a menu item can carry [#what-a-menu-item-can-carry]
A `MenuItem` can already include:
* price in cents
* rich description content
* images
* availability
* featured and popular flags
* prep time
* calorie count
* kitchen station
* allergen tags
* dietary flags
* meal-period tags
* a category link
* one or more modifiers
That is enough to drive both the storefront and the kitchen-facing workflow.
Modifiers are first-class data [#modifiers-are-first-class-data]
Modifier groups already support:
* required or optional selection
* min and max selections
* default-selected values
* price adjustments
* group labels like "Choose your side" or "Add-ons"
That makes them useful for both guest ordering and staff-assisted ordering.
How to build the menu [#how-to-build-the-menu]
Create categories [#create-categories]
Start with `MenuCategory` records. These control menu structure, sorting, and meal-period tagging.
Add the items [#add-the-items]
Create `MenuItem` records with pricing, imagery, availability, station routing, and customer-facing details.
Attach modifiers [#attach-modifiers]
Use `MenuItemModifier` records to define sizes, toppings, sides, sauces, removals, temperatures, and similar choice sets.
Publish through the same data layer [#publish-through-the-same-data-layer]
The storefront, POS, and KDS all read from the same underlying menu records. You are not maintaining three different menus.
What the current menu tooling is good at [#what-the-current-menu-tooling-is-good-at]
* one shared menu for guest and staff surfaces
* clean category sorting
* item imagery and featured sections on the storefront
* availability control for sold-out items
* dietary and allergen tagging
* kitchen station routing from the item record
* modifier pricing and validation rules
What is present but not fully matured yet [#what-is-present-but-not-fully-matured-yet]
Meal periods are already modeled and filterable through the API, but the platform is not yet doing a deeply automated schedule-driven menu swap by itself. The fields are there. The last mile still depends on how far you want to take it.
If you only have time for one careful setup pass before launch, spend it here. A clean menu model pays off everywhere else in the restaurant.
# Inventory and food costing
Inventory is where a restaurant platform either becomes useful or stays decorative.
Openfront Restaurant already includes the core pieces you need to connect menu sales to ingredient cost and stock movement.
What is in the inventory layer [#what-is-in-the-inventory-layer]
Ingredients [#ingredients]
Ingredients track:
* stock quantity
* unit of measure
* category
* par level
* reorder point and reorder quantity
* cost per unit
* vendor
* storage location
* expiration date and SKU
Recipes [#recipes]
Recipes link menu items to their ingredient usage. Each recipe can store:
* the linked menu item
* ingredient list as structured JSON
* yield
* prep time
* instructions
* total cost
* cost per serving
* food cost percentage
Purchasing and waste [#purchasing-and-waste]
The platform also includes:
* vendors
* inventory locations
* purchase orders
* stock movements
* waste logs
The practical workflow [#the-practical-workflow]
Create ingredients first [#create-ingredients-first]
Add your raw materials with unit, cost, vendor, and baseline stock information.
Build recipes for menu items [#build-recipes-for-menu-items]
Create a recipe record, link it to a menu item, and add the ingredients that make up that dish.
Review food cost [#review-food-cost]
Once the recipe is in place, the app can calculate total cost, cost per serving, and food cost percentage.
Complete real orders [#complete-real-orders]
When an order is completed and a linked recipe exists, the order hooks can deplete ingredient stock automatically and write stock-movement records.
Track purchasing and waste [#track-purchasing-and-waste]
Use purchase orders to manage restocking and waste logs to understand where margin is leaking.
Why this part matters [#why-this-part-matters]
A lot of restaurant software stops at "we sold the burger." The better question is whether you sold it at a number that still makes sense once the bun, beef, cheese, labor, and waste are all in the picture.
This build is already moving in that direction.
Current strengths [#current-strengths]
* ingredient records are detailed enough for real kitchen use
* recipes already calculate cost metrics
* waste has a dedicated model and screen
* purchase orders have a useful workflow and status model
* completed orders can write stock movement automatically
Current gaps [#current-gaps]
* recipe ingredients are stored as JSON today, so this layer works best when your team is comfortable with structured admin data
* unit conversion and purchasing normalization still have room to grow
* the value of this module jumps once you commit to keeping recipe data current
The inventory module is already worth documenting because it does real work. It just gets much better as your operational discipline improves.
# Point of sale
The POS screen is built for speed. It is the place where a server, cashier, or counter staff member can open a new order without bouncing between three different tools.
In the current build, the POS is strongest at the front half of service: opening orders, assigning tables, grouping courses, and sending the order into the restaurant workflow.
What the POS already does well [#what-the-pos-already-does-well]
* switch between dine-in and takeout
* select one or more active tables for a dine-in order
* browse menu items by category
* respect item availability and show sold-out items as unavailable
* build a cart with quantities
* assign each line item to course 1, 2, or 3
* mark the whole order as urgent
* add order-level special instructions
* create `RestaurantOrder`, `OrderCourse`, and `OrderItem` records in one flow
Typical POS flow [#typical-pos-flow]
Choose the order type [#choose-the-order-type]
Start with dine-in or takeout. If it is dine-in, pick the table or tables that should own the check.
Build the cart [#build-the-cart]
Tap through categories, add items, and adjust quantities. Unavailable items are visible, but they cannot be added by mistake.
Set the service timing [#set-the-service-timing]
Each line item can be assigned to course 1, 2, or 3. That gives the order structure before it ever reaches the service floor or kitchen workflow.
Add urgency or special instructions [#add-urgency-or-special-instructions]
If the order needs extra attention, mark it urgent. You can also add notes for allergies, timing, or kitchen context.
Send the order [#send-the-order]
Submitting the cart creates the restaurant order, creates course records, creates order items, and links tables when the order is dine-in.
What happens after submission [#what-happens-after-submission]
When the POS creates a dine-in order, the order hooks can immediately mark the linked tables as occupied. From there, the order can move into the kitchen pipeline and later into the payment workflow.
The POS is intentionally lean. It does not try to handle every mid-service edge case itself.
What lives outside the initial POS screen [#what-lives-outside-the-initial-pos-screen]
These workflows happen elsewhere in the platform after the order is open:
* split check by guest or by item
* combine tables or transfer a check to a new table
* fire or recall courses during service
* close the check with cash, card, split payment, or gift card
Those tools live mainly in the service-floor and payment pages.
A good way to think about it: the POS opens the order fast. The service-floor UI runs the dining room once that order is live.
Current limitations [#current-limitations]
* The current POS creation flow is lighter on modifier selection than the storefront customization flow.
* There is no offline sync mode yet.
* Seat-by-seat ordering is not the center of this screen. More complex service actions happen after the order is open.
# Reporting
Openfront Restaurant already has four reporting surfaces that matter day to day. They are not pretending to be a giant BI warehouse. They are there to help an operator answer the obvious questions after a shift.
The reporting surfaces [#the-reporting-surfaces]
What each report is using [#what-each-report-is-using]
Operational dashboard [#operational-dashboard]
The operational dashboard pulls together live counts like:
* open orders
* in-progress orders
* ready orders
* occupied tables
* today's revenue and order count
It is best treated as the quick pulse of the restaurant, not as a historical analysis tool.
Sales report [#sales-report]
The sales report is the strongest of the current views. It already rolls up:
* total revenue
* completed orders
* average check
* total guests
* total tax, tips, and discounts
* daypart trends
* order-type mix
* payment-method mix
Menu performance [#menu-performance]
Menu performance is driven by completed `OrderItem` records. It helps you spot:
* which items actually move
* which categories are carrying revenue
* which menu items are dragging
* where recipe costing would make the view even sharper
Labor report [#labor-report]
Labor reporting combines `TimeEntry` data with completed-order sales. It is useful for:
* payroll cost review
* labor percentage
* sales per labor hour
* role-by-role cost breakdown
Where reporting is still early [#where-reporting-is-still-early]
A few pieces are present but still rough:
* some operational metrics are still more dashboard hints than finished analytics products
* recipe-linked profitability gets much better once your inventory and recipe data are filled out
* export and long-range forecasting are not the point of the current build yet
The reporting story is already useful for operating a restaurant. It just is not trying to be everything at once.
# Staff and labor
Openfront Restaurant already has more than a basic user table. Staff records include restaurant-specific fields, and there are working screens for scheduling, tip pooling, and labor analysis.
Staff data that already exists [#staff-data-that-already-exists]
A user record can store:
* restaurant staff role
* employee ID
* hourly rate
* phone number and photo
* PIN field for quick workflows
* onboarding status
* emergency contact details
* certifications
* active or inactive status
That gives you a decent base for real operations, not just login management.
Main labor surfaces [#main-labor-surfaces]
Weekly schedule [#weekly-schedule]
The schedule screen is a weekly roster view where managers can:
* assign shifts by day
* attach a staff member and role
* set start and end times
* store hourly rate per shift
* edit or delete shifts later
Tip hub [#tip-hub]
Tip pools are already modeled and have a dedicated screen for:
* house pool distribution by hours worked
* role-based weighted pools
* settled and unsettled batches
* viewing calculated distributions before marking a batch distributed
Labor report [#labor-report]
The labor report pulls together time entries and completed-order sales to show:
* total hours
* payroll cost
* labor percentage
* sales per labor hour
* total tips
* role-level breakdowns
How teams usually use it [#how-teams-usually-use-it]
Create and maintain staff records [#create-and-maintain-staff-records]
Start with the user record. Add the staff role, hourly rate, phone, and any emergency or certification data you want to keep close.
Build the schedule [#build-the-schedule]
Use the weekly schedule screen to assign shifts and keep the roster visible by day.
Record worked time [#record-worked-time]
Time-entry data and shift clock fields are already part of the model layer and feed reporting. That is what the labor and tip views depend on.
Review labor after service [#review-labor-after-service]
Use the labor report and tip hub to understand whether the shift made sense financially, not just whether it felt busy.
Current limitations [#current-limitations]
* The reporting side of labor is stronger than the staff-clock experience right now.
* Permissions around who can manage people, roles, and wage data still need a cleanup pass.
The staff module is already useful for scheduling and labor visibility. The next big win here is polishing the operational clock-in and permissions story.
# Storefront ordering
Openfront Restaurant includes a customer-facing menu and ordering application. Current routes cover the menu, cart checkout, order confirmation, sign-in, profile, addresses, and customer order history.
Current routes [#current-routes]
* `/` for the menu and cart entry;
* `/menu/[id]` for an item page;
* `/checkout` for contact, delivery, payment, and review steps;
* `/order/confirmed/[id]` for confirmation;
* `/account`, `/account/profile`, `/account/addresses`, and account order routes.
The storefront implementation lives under `app/(storefront)` and `features/storefront`. Keystone models and custom GraphQL operations remain the backend contract.
What drives the storefront [#what-drives-the-storefront]
* `StoreSettings` supplies branding, locale, currency, hours, delivery rules, tax, and pickup settings.
* `MenuCategory`, `MenuItem`, and `MenuItemModifier` supply menu content and customization.
* `Cart` and `CartItem` hold checkout state.
* `PaymentCollection` and `PaymentSession` hold the selected provider session.
* `RestaurantOrder`, `OrderItem`, and `Payment` are created when checkout completes.
Current checkout sequence [#current-checkout-sequence]
Create or resume a cart [#create-or-resume-a-cart]
The server stores a cart ID in the `_restaurant_cart_id` cookie. Cart reads and writes call custom operations that check the authenticated user or the matching cart cookie before using privileged Keystone access.
Add menu items and checkout details [#add-menu-items-and-checkout-details]
The cart stores item quantities, modifiers, special instructions, contact data, pickup or delivery mode, and delivery fields. Guest contact submission currently creates a user record when no signed-in user exists and connects it to the cart.
Validate delivery and calculate totals [#validate-delivery-and-calculate-totals]
Checkout normalizes the address and checks the configured delivery mode, country, postal code, and minimum. `completeActiveCart` recalculates tax, tip, pickup discount, delivery fee, and total from current cart and StoreSettings data.
Select and confirm payment [#select-and-confirm-payment]
The storefront initiates a `PaymentSession`. Stripe confirms card payment in the browser; PayPal uses its order approval flow; the manual provider records pending payment. Order completion checks or captures non-manual payment through the configured adapter before creating the order.
Create the order [#create-the-order]
`completeActiveCart` creates the `RestaurantOrder`, its `OrderItem` rows, a `Payment`, the cart-to-order link, and kitchen tickets for kitchen-active statuses. The confirmation page then reads the resulting order.
Checkout mutation [#checkout-mutation]
After a payment session has been selected and, where required, approved by the provider, the storefront completes the cart with this custom operation:
```graphql
mutation CompleteRestaurantCart($cartId: ID!, $paymentSessionId: ID) {
completeActiveCart(cartId: $cartId, paymentSessionId: $paymentSessionId) {
id
orderNumber
status
secretKey
}
}
```
Use the generated `schema.graphql` from the same source revision. Do not replace this workflow with unrestricted generated CRUD from an untrusted client.
Order access [#order-access]
`getCustomerOrder(orderId, secretKey)` returns an order to its authenticated customer. It also accepts the exact `secretKey` for an order that has one. Treat that value as a credential: keep it out of analytics, logs, referrers, screenshots, and shared caches, and test that wrong-user and wrong-secret requests return no order details.
Current payment paths [#current-payment-paths]
The storefront renders Stripe, PayPal, and manual payment controls when their provider sessions are selected. Availability still depends on installed provider records, environment configuration, credentials, adapter responses, and the exact checkout path being tested.
Current limitations [#current-limitations]
Current completion performs several privileged writes and provider calls without one encompassing database transaction or a caller-supplied idempotency key. Test retries, duplicate submission, partial order/item/payment writes, provider reconciliation, and kitchen-ticket recovery before accepting live orders.
* The application exposes no `/api/categories`, `/api/menu-items`, or `/api/orders/*` REST routes in current source. External clients should use reviewed GraphQL operations or add a separately authenticated API contract.
* The customer storefront does not include a reservations flow.
* A cart cookie contains the cart ID rather than a separate scoped cart token; cookie security and cross-owner negative tests remain important.
* Provider support in the UI does not establish end-to-end readiness for refunds, webhooks, retries, or reconciliation.
* Generated list CRUD remains distinct from the safer custom cart and order workflows and should not be exposed merely to simplify another client.
Where to go next [#where-to-go-next]
# Waitlist and reservations
The waitlist flow is already one of the cleaner operational pieces in the product. Hosts can add parties, track quoted wait time, mark guests as notified, and seat them into tables that actually fit.
What the waitlist screen does [#what-the-waitlist-screen-does]
* add a party with name, phone number, party size, quoted wait, and notes
* keep the list focused on parties that are still waiting or already notified
* move a party from waiting to notified
* seat a party into a matching available table
* cancel a party or mark them as a no-show
Seating workflow [#seating-workflow]
Add the party [#add-the-party]
Create a waitlist entry with guest name, phone number, party size, quoted wait time, and optional notes.
Keep the status honest [#keep-the-status-honest]
When the host reaches out, mark the guest as notified. If they disappear, cancel them or mark them as a no-show.
Match the right table [#match-the-right-table]
When it is time to seat the party, the screen loads available tables with enough capacity for that group.
Seat the party [#seat-the-party]
Seating the guest updates the waitlist entry and flips the table to occupied at the same time.
What about reservations? [#what-about-reservations]
Reservations already exist in the data model and in the dashboard route. A reservation stores:
* guest name and contact details
* reservation date and party size
* duration
* status
* special requests
* assigned table
That said, reservations are not as fully built out as the waitlist flow yet. Today they are better described as a solid model and admin surface than a finished host-stand product.
The current waitlist handles real host work better than the reservations flow. If your launch depends heavily on formal reservation management, plan to spend more time there before calling it done.
What is not wired yet [#what-is-not-wired-yet]
The waitlist stores phone numbers and a notified state, but it does not send SMS messages by itself yet. If you want text messaging, you will need to add that integration on top of the existing workflow.