A Ghost Kitchen has no human staff to take orders, cook food, or hand it over at the counter. Customers still need a way to browse a menu, order food that will be physically prepared by automated Machines (oven, grill, etc.), pay for it, know when and where to pick it up, and — if they change their mind before their food is actually being cooked — cancel it. Right now none of this exists: there is no menu, no ordering flow, no order tracking, and no way for a customer's payment to trigger anything.
Solution
A web app (TanStack Start + Postgres + Drizzle) where a customer browses a single Brand's menu, places an Order containing one or more Order Items, and pays online up front. Each Order Item is tracked independently through its own Preparation Steps (each requiring a Machine carrying a specific Machine Tag), can be cancelled and refunded up until its first step starts, becomes Ready and is picked up independently of other items in the same Order, and is visible to the customer as a live queue position / step list / ETA. Real hardware control of Machines is Phase 2; this spec covers only the ordering, payment, queuing-domain, and status-tracking software, with an internal admin action standing in for a Machine finishing a step.
User Stories
As a customer, I want to browse the Kitchen's Brand menu, so that I can see what food is available to order.
As a customer, I want to add one or more menu items to a cart, so that I can order multiple things (e.g. a pizza and a burger) in one Order.
As a customer, I want to place an Order as a guest (name + contact only), so that I can order without creating an account.
As a customer, I want to create an account and log in, so that I can check out faster and see my order history on future visits.
As a customer, I want to pay online at the moment I place my Order, so that my food starts being prepared immediately without needing anyone at a counter to take payment.
As a customer, I want my Order to be rejected if payment fails, so that no food is ever queued for preparation without being paid for.
As a customer, I want each Order Item I ordered to start queuing for preparation as soon as payment succeeds, so that independent items don't wait on each other before they even begin.
As a customer, I want to see, for each Order Item, the full list of Preparation Steps it needs to go through, so that I understand what's happening to my food.
As a customer, I want to see my Order Item's current position in the queue for its next required Machine Tag, so that I know roughly how long until it starts cooking.
As a customer, I want to see an approximate time-to-completion for each Order Item, so that I know when to head to the pickup counter.
As a customer, I want to cancel a single Order Item and be refunded for it, as long as its first Preparation Step hasn't started yet, so that I can correct a mistake without losing money on food that hasn't been touched.
As a customer, I want to be told an Order Item can no longer be cancelled once its first Preparation Step has started, so that I understand why my cancellation attempt was refused.
As a customer, I want cancelling one Order Item in my Order to have no effect on the other items in that Order, so that a mistake on one item doesn't force me to lose or restart the whole Order.
As a customer, I want to be notified as soon as an individual Order Item becomes Ready, so that I can go pick it up while it's fresh, without waiting for slower items in the same Order.
As a customer, I want to be notified once every Order Item in my Order has been picked up, so that I know my whole Order is complete.
As a customer, I want to pick up each Order Item separately at the pickup counter as it becomes Ready, so that I'm not forced to wait for the slowest item before collecting anything.
As a customer, I want to confirm that I've collected an Order Item at the counter, so that the system knows it's been picked up and can mark the Order complete once everything has been collected.
As a customer, I want to see my Order's overall status (e.g. how many of its items are still preparing vs. ready vs. picked up), so that I have one place to check on the whole thing.
As a kitchen operator/developer, I want an internal action that simulates a Machine finishing an Order Item's current Preparation Step, so that the full ordering-and-queuing flow can be built and tested before real microcontroller-driven Machines are wired up.
As a kitchen operator/developer, I want that simulated "step finished" action to move the Order Item to its next Preparation Step (or to Ready, if it was the last step), so that the internal stand-in behaves like a real Machine would.
As the business deploying this software, I want the Brand (name, menu, Machines, Machine Tags) to be configurable per Kitchen deployment rather than hardcoded, so that the same codebase can be deployed as a template for a different Kitchen/Brand later without a code change.
As the business, I want each Kitchen to run against its own independent Postgres database with no cross-Kitchen data access, so that a Kitchen's server only ever needs to know about its own Machines and orders.
As a developer, I want a Machine Tag to represent a pool of one or more interchangeable Machines (e.g. two ovens), so that an Order Item's Preparation Step can be dynamically assigned to whichever Machine of the required Tag is idle, instead of being pinned to one specific Machine.
As a developer, I want a menu item's required Preparation Steps (their sequence and required Machine Tags) to be defined as data on the menu item, so that adding a new multi-step recipe later doesn't require new ordering-flow code.
As a developer, I want the Order Item state machine (queued → preparing per step → ready → picked up, with a derived Order-level "complete" state) to be enforced server-side, so that invalid transitions (e.g. picking up an item that hasn't finished its steps) are rejected rather than silently accepted.
Implementation Decisions
Entities: Kitchen (one per deployment, owns one Brand), Brand (name/branding, one per Kitchen), MenuItem (belongs to a Brand; has a Recipe — an ordered list of Preparation Step definitions, each naming a required MachineTag and an estimated duration), Machine (belongs to a Kitchen, carries one or more MachineTags), MachineTag (capability label, e.g. oven, grill, assembly), Order (belongs to a Customer or a guest identity, holds one or more OrderItems), OrderItem (references a MenuItem, has its own ordered list of PreparationStepInstances mirroring the MenuItem's Recipe), PreparationStepInstance (status: queued / in_progress / done, and which Machine it was ultimately assigned to).
Order Item lifecycle (per ADR-0002, tracked per Order Item, not per Order): placed → paid → (its steps run in sequence, each queued → in_progress → done against its required Machine Tag's pool, per ADR-0003) → ready → picked_up. The Order's own status is derived: complete once every OrderItem is picked_up.
Payment: online only, charged at Order placement, before any OrderItem enters a queue. Default to Stripe Payment Intents for the payment provider (concrete choice made for this spec; swappable later) — chosen because it supports clean partial/per-item refunds, which the cancellation flow needs. A successful payment webhook/confirmation is what triggers creating each OrderItem's first PreparationStepInstance in the queued state.
Cancellation: per OrderItem. Allowed (with refund of that item's price only) any time before its first PreparationStepInstance transitions to in_progress. Once in_progress, that OrderItem is locked — no cancellation, no refund. Cancelling one OrderItem never affects sibling OrderItems in the same Order.
Machine Tag pool assignment (ADR-0003): a PreparationStepInstance queues against its required MachineTag, not a specific Machine. When any Machine carrying that Tag goes idle, it is dynamically assigned the next queued step for that Tag (FIFO by queue position).
Customer-visible status: for each OrderItem, expose its full Preparation Step list (from its MenuItem's Recipe), which step is current, that step's queue position within its MachineTag's queue, and an approximate ETA computed from queue position × steps' estimated durations. Internal step-by-step detail ("grilling" vs. "assembling") is shown to the customer only as an aggregate "Preparing" — full Step detail is available but the customer view collapses it to Preparing/Ready.
Manual step-advance action: an internal action (not part of the Operator role) that marks an OrderItem's current in_progress PreparationStepInstance as done, advancing it to its next Step (queued again) or to ready if it was the last Step. This stands in for real Machine/microcontroller completion until Phase 2 hardware integration.
Notifications: per-OrderItem "ready" notification, plus one further notification once the whole Order reaches complete. For this spec, delivered as live status updates within the customer's own web session (e.g. polling or a push-based subscription from the server function layer) — not native mobile push or SMS (see Out of Scope).
Template architecture (ADR-0001): Brand name/branding, menu, Machines, and Machine Tags are seeded/configured data per Kitchen deployment, not hardcoded, even though each Kitchen deployment has its own independent server and Postgres database with no cross-Kitchen data access.
Guest vs. account checkout: both supported. A guest Order records name + contact info directly on the Order; an account Order links to a Customer record. Both go through the same Order/OrderItem/payment flow.
Testing Decisions
Tests call TanStack Start server functions directly (in-process, no HTTP round-trip, no browser) and assert on both the function's return value and the resulting rows in a real Postgres database via Drizzle queries — this is the single seam for this feature, per the confirmed test-seam decision.
Use a real, ephemeral test Postgres instance (not an in-memory fake or a mocked Drizzle client) with migrations applied fresh per test run/reset between tests, so tests exercise real SQL and real constraints.
Only test external behavior: given a sequence of server-function calls (place order, cancel item, advance step, mark picked up), assert on the observable state (order/item statuses, queue positions, refund amounts) — never assert on internal call counts or implementation details.
The one true external dependency worth abstracting behind a swappable interface is the payment provider (Stripe): use Stripe's test-mode API for realistic behavior where practical, falling back to a fake payment provider implementing the same interface if test-mode calls prove too slow/rate-limited for the test suite.
No prior art exists yet in this codebase (greenfield) — these tests establish the pattern that later features should follow.
Out of Scope
Real microcontroller/hardware integration with physical Machines (Phase 2).
A physical in-store kiosk/status-board display (Phase 2 hardware rollout).
Native mobile push notifications or SMS delivery (a specific notification provider hasn't been chosen; only in-session live status is built now).
Operator-facing tooling for restocking ingredients or machine repairs (a separate, physical-world concern, not part of the ordering flow).
Multiple Brands per Kitchen, and multiple Kitchen locations/selection UI (this project is explicitly one Brand per Kitchen, one Kitchen per deployment).
Cross-Kitchen admin/analytics (explicitly out of scope per ADR-0001 — no cross-Kitchen data layer).
Taxes, tips, promotions/discounts, and refund disputes/chargebacks.
Further Notes
Domain vocabulary and full definitions: CONTEXT.md.
Architectural decisions this spec builds on: docs/adr/0001-one-server-per-kitchen.md, docs/adr/0002-order-items-are-independent.md, docs/adr/0003-machine-tag-pool-dynamic-assignment.md.
Specific menu items (pizza, burger, etc.) and their exact Recipes/durations are explicitly deferred — this spec builds the general MenuItem/Recipe/PreparationStep data model, not any particular item's content.
Stripe was chosen as a concrete default for the payment provider so this spec could be specific about refund behavior; treat it as a starting point, not a hard requirement, if the developer prefers a different provider.
## Problem Statement
A Ghost Kitchen has no human staff to take orders, cook food, or hand it over at the counter. Customers still need a way to browse a menu, order food that will be physically prepared by automated Machines (oven, grill, etc.), pay for it, know when and where to pick it up, and — if they change their mind before their food is actually being cooked — cancel it. Right now none of this exists: there is no menu, no ordering flow, no order tracking, and no way for a customer's payment to trigger anything.
## Solution
A web app (TanStack Start + Postgres + Drizzle) where a customer browses a single Brand's menu, places an Order containing one or more Order Items, and pays online up front. Each Order Item is tracked independently through its own Preparation Steps (each requiring a Machine carrying a specific Machine Tag), can be cancelled and refunded up until its first step starts, becomes Ready and is picked up independently of other items in the same Order, and is visible to the customer as a live queue position / step list / ETA. Real hardware control of Machines is Phase 2; this spec covers only the ordering, payment, queuing-domain, and status-tracking software, with an internal admin action standing in for a Machine finishing a step.
## User Stories
1. As a customer, I want to browse the Kitchen's Brand menu, so that I can see what food is available to order.
2. As a customer, I want to add one or more menu items to a cart, so that I can order multiple things (e.g. a pizza and a burger) in one Order.
3. As a customer, I want to place an Order as a guest (name + contact only), so that I can order without creating an account.
4. As a customer, I want to create an account and log in, so that I can check out faster and see my order history on future visits.
5. As a customer, I want to pay online at the moment I place my Order, so that my food starts being prepared immediately without needing anyone at a counter to take payment.
6. As a customer, I want my Order to be rejected if payment fails, so that no food is ever queued for preparation without being paid for.
7. As a customer, I want each Order Item I ordered to start queuing for preparation as soon as payment succeeds, so that independent items don't wait on each other before they even begin.
8. As a customer, I want to see, for each Order Item, the full list of Preparation Steps it needs to go through, so that I understand what's happening to my food.
9. As a customer, I want to see my Order Item's current position in the queue for its next required Machine Tag, so that I know roughly how long until it starts cooking.
10. As a customer, I want to see an approximate time-to-completion for each Order Item, so that I know when to head to the pickup counter.
11. As a customer, I want to cancel a single Order Item and be refunded for it, as long as its first Preparation Step hasn't started yet, so that I can correct a mistake without losing money on food that hasn't been touched.
12. As a customer, I want to be told an Order Item can no longer be cancelled once its first Preparation Step has started, so that I understand why my cancellation attempt was refused.
13. As a customer, I want cancelling one Order Item in my Order to have no effect on the other items in that Order, so that a mistake on one item doesn't force me to lose or restart the whole Order.
14. As a customer, I want to be notified as soon as an individual Order Item becomes Ready, so that I can go pick it up while it's fresh, without waiting for slower items in the same Order.
15. As a customer, I want to be notified once every Order Item in my Order has been picked up, so that I know my whole Order is complete.
16. As a customer, I want to pick up each Order Item separately at the pickup counter as it becomes Ready, so that I'm not forced to wait for the slowest item before collecting anything.
17. As a customer, I want to confirm that I've collected an Order Item at the counter, so that the system knows it's been picked up and can mark the Order complete once everything has been collected.
18. As a customer, I want to see my Order's overall status (e.g. how many of its items are still preparing vs. ready vs. picked up), so that I have one place to check on the whole thing.
19. As a kitchen operator/developer, I want an internal action that simulates a Machine finishing an Order Item's current Preparation Step, so that the full ordering-and-queuing flow can be built and tested before real microcontroller-driven Machines are wired up.
20. As a kitchen operator/developer, I want that simulated "step finished" action to move the Order Item to its next Preparation Step (or to Ready, if it was the last step), so that the internal stand-in behaves like a real Machine would.
21. As the business deploying this software, I want the Brand (name, menu, Machines, Machine Tags) to be configurable per Kitchen deployment rather than hardcoded, so that the same codebase can be deployed as a template for a different Kitchen/Brand later without a code change.
22. As the business, I want each Kitchen to run against its own independent Postgres database with no cross-Kitchen data access, so that a Kitchen's server only ever needs to know about its own Machines and orders.
23. As a developer, I want a Machine Tag to represent a pool of one or more interchangeable Machines (e.g. two ovens), so that an Order Item's Preparation Step can be dynamically assigned to whichever Machine of the required Tag is idle, instead of being pinned to one specific Machine.
24. As a developer, I want a menu item's required Preparation Steps (their sequence and required Machine Tags) to be defined as data on the menu item, so that adding a new multi-step recipe later doesn't require new ordering-flow code.
25. As a developer, I want the Order Item state machine (queued → preparing per step → ready → picked up, with a derived Order-level "complete" state) to be enforced server-side, so that invalid transitions (e.g. picking up an item that hasn't finished its steps) are rejected rather than silently accepted.
## Implementation Decisions
- **Entities**: `Kitchen` (one per deployment, owns one `Brand`), `Brand` (name/branding, one per Kitchen), `MenuItem` (belongs to a Brand; has a `Recipe` — an ordered list of Preparation Step definitions, each naming a required `MachineTag` and an estimated duration), `Machine` (belongs to a Kitchen, carries one or more `MachineTag`s), `MachineTag` (capability label, e.g. `oven`, `grill`, `assembly`), `Order` (belongs to a Customer or a guest identity, holds one or more `OrderItem`s), `OrderItem` (references a `MenuItem`, has its own ordered list of `PreparationStepInstance`s mirroring the MenuItem's Recipe), `PreparationStepInstance` (status: `queued` / `in_progress` / `done`, and which `Machine` it was ultimately assigned to).
- **Order Item lifecycle** (per ADR-0002, tracked per Order Item, not per Order): `placed` → `paid` → (its steps run in sequence, each `queued` → `in_progress` → `done` against its required Machine Tag's pool, per ADR-0003) → `ready` → `picked_up`. The Order's own status is derived: `complete` once every OrderItem is `picked_up`.
- **Payment**: online only, charged at Order placement, before any OrderItem enters a queue. Default to Stripe Payment Intents for the payment provider (concrete choice made for this spec; swappable later) — chosen because it supports clean partial/per-item refunds, which the cancellation flow needs. A successful payment webhook/confirmation is what triggers creating each OrderItem's first `PreparationStepInstance` in the `queued` state.
- **Cancellation**: per OrderItem. Allowed (with refund of that item's price only) any time before its first PreparationStepInstance transitions to `in_progress`. Once `in_progress`, that OrderItem is locked — no cancellation, no refund. Cancelling one OrderItem never affects sibling OrderItems in the same Order.
- **Machine Tag pool assignment** (ADR-0003): a PreparationStepInstance queues against its required MachineTag, not a specific Machine. When any Machine carrying that Tag goes idle, it is dynamically assigned the next `queued` step for that Tag (FIFO by queue position).
- **Customer-visible status**: for each OrderItem, expose its full Preparation Step list (from its MenuItem's Recipe), which step is current, that step's queue position within its MachineTag's queue, and an approximate ETA computed from queue position × steps' estimated durations. Internal step-by-step detail ("grilling" vs. "assembling") is shown to the customer only as an aggregate "Preparing" — full Step detail is available but the customer view collapses it to Preparing/Ready.
- **Manual step-advance action**: an internal action (not part of the Operator role) that marks an OrderItem's current `in_progress` PreparationStepInstance as `done`, advancing it to its next Step (`queued` again) or to `ready` if it was the last Step. This stands in for real Machine/microcontroller completion until Phase 2 hardware integration.
- **Notifications**: per-OrderItem "ready" notification, plus one further notification once the whole Order reaches `complete`. For this spec, delivered as live status updates within the customer's own web session (e.g. polling or a push-based subscription from the server function layer) — not native mobile push or SMS (see Out of Scope).
- **Template architecture** (ADR-0001): Brand name/branding, menu, Machines, and Machine Tags are seeded/configured data per Kitchen deployment, not hardcoded, even though each Kitchen deployment has its own independent server and Postgres database with no cross-Kitchen data access.
- **Guest vs. account checkout**: both supported. A guest Order records name + contact info directly on the Order; an account Order links to a Customer record. Both go through the same Order/OrderItem/payment flow.
## Testing Decisions
- Tests call TanStack Start server functions directly (in-process, no HTTP round-trip, no browser) and assert on both the function's return value and the resulting rows in a real Postgres database via Drizzle queries — this is the single seam for this feature, per the confirmed test-seam decision.
- Use a real, ephemeral test Postgres instance (not an in-memory fake or a mocked Drizzle client) with migrations applied fresh per test run/reset between tests, so tests exercise real SQL and real constraints.
- Only test external behavior: given a sequence of server-function calls (place order, cancel item, advance step, mark picked up), assert on the observable state (order/item statuses, queue positions, refund amounts) — never assert on internal call counts or implementation details.
- The one true external dependency worth abstracting behind a swappable interface is the payment provider (Stripe): use Stripe's test-mode API for realistic behavior where practical, falling back to a fake payment provider implementing the same interface if test-mode calls prove too slow/rate-limited for the test suite.
- No prior art exists yet in this codebase (greenfield) — these tests establish the pattern that later features should follow.
## Out of Scope
- Real microcontroller/hardware integration with physical Machines (Phase 2).
- A physical in-store kiosk/status-board display (Phase 2 hardware rollout).
- Native mobile push notifications or SMS delivery (a specific notification provider hasn't been chosen; only in-session live status is built now).
- Operator-facing tooling for restocking ingredients or machine repairs (a separate, physical-world concern, not part of the ordering flow).
- Multiple Brands per Kitchen, and multiple Kitchen locations/selection UI (this project is explicitly one Brand per Kitchen, one Kitchen per deployment).
- Cross-Kitchen admin/analytics (explicitly out of scope per ADR-0001 — no cross-Kitchen data layer).
- Taxes, tips, promotions/discounts, and refund disputes/chargebacks.
## Further Notes
- Domain vocabulary and full definitions: `CONTEXT.md`.
- Architectural decisions this spec builds on: `docs/adr/0001-one-server-per-kitchen.md`, `docs/adr/0002-order-items-are-independent.md`, `docs/adr/0003-machine-tag-pool-dynamic-assignment.md`.
- Specific menu items (pizza, burger, etc.) and their exact Recipes/durations are explicitly deferred — this spec builds the general MenuItem/Recipe/PreparationStep data model, not any particular item's content.
- Stripe was chosen as a concrete default for the payment provider so this spec could be specific about refund behavior; treat it as a starting point, not a hard requirement, if the developer prefers a different provider.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem Statement
A Ghost Kitchen has no human staff to take orders, cook food, or hand it over at the counter. Customers still need a way to browse a menu, order food that will be physically prepared by automated Machines (oven, grill, etc.), pay for it, know when and where to pick it up, and — if they change their mind before their food is actually being cooked — cancel it. Right now none of this exists: there is no menu, no ordering flow, no order tracking, and no way for a customer's payment to trigger anything.
Solution
A web app (TanStack Start + Postgres + Drizzle) where a customer browses a single Brand's menu, places an Order containing one or more Order Items, and pays online up front. Each Order Item is tracked independently through its own Preparation Steps (each requiring a Machine carrying a specific Machine Tag), can be cancelled and refunded up until its first step starts, becomes Ready and is picked up independently of other items in the same Order, and is visible to the customer as a live queue position / step list / ETA. Real hardware control of Machines is Phase 2; this spec covers only the ordering, payment, queuing-domain, and status-tracking software, with an internal admin action standing in for a Machine finishing a step.
User Stories
Implementation Decisions
Kitchen(one per deployment, owns oneBrand),Brand(name/branding, one per Kitchen),MenuItem(belongs to a Brand; has aRecipe— an ordered list of Preparation Step definitions, each naming a requiredMachineTagand an estimated duration),Machine(belongs to a Kitchen, carries one or moreMachineTags),MachineTag(capability label, e.g.oven,grill,assembly),Order(belongs to a Customer or a guest identity, holds one or moreOrderItems),OrderItem(references aMenuItem, has its own ordered list ofPreparationStepInstances mirroring the MenuItem's Recipe),PreparationStepInstance(status:queued/in_progress/done, and whichMachineit was ultimately assigned to).placed→paid→ (its steps run in sequence, eachqueued→in_progress→doneagainst its required Machine Tag's pool, per ADR-0003) →ready→picked_up. The Order's own status is derived:completeonce every OrderItem ispicked_up.PreparationStepInstancein thequeuedstate.in_progress. Oncein_progress, that OrderItem is locked — no cancellation, no refund. Cancelling one OrderItem never affects sibling OrderItems in the same Order.queuedstep for that Tag (FIFO by queue position).in_progressPreparationStepInstance asdone, advancing it to its next Step (queuedagain) or toreadyif it was the last Step. This stands in for real Machine/microcontroller completion until Phase 2 hardware integration.complete. For this spec, delivered as live status updates within the customer's own web session (e.g. polling or a push-based subscription from the server function layer) — not native mobile push or SMS (see Out of Scope).Testing Decisions
Out of Scope
Further Notes
CONTEXT.md.docs/adr/0001-one-server-per-kitchen.md,docs/adr/0002-order-items-are-independent.md,docs/adr/0003-machine-tag-pool-dynamic-assignment.md.