A Point of Sale system built for a coffee shop, running on web, mobile and a customer-facing ordering app. It covers the catalog (categories, products, variants, materials), sales (transactions, carts, tables, tickets, coupons), inventory (suppliers, stock checks), finance (expenses, budgets, wallets, profit calculation) and daily operations (checklists, rentals).
The full feature documentation lives at
gatherloop.github.io/gatherloop-pos (source in
docs-site/).
An Nx monorepo. Apps are thin shells; nearly all frontend code lives in
libs/ui, shared by web, mobile and order.
apps/
api/ Go backend (REST API, MySQL)
pos-web/ Next.js admin/cashier app (Pages Router)
order-web/ Next.js customer app — scan a table QR, order and pay by QRIS from your phone
pos-mobile/ React Native (Expo) app for iOS/Android
kds-mobile/ React Native (Expo) app — a dedicated phone that buzzes when an order is paid
*-e2e/ Playwright end-to-end tests per app
libs/
ui/ All shared frontend code: entities, use cases, screens, components
src/app/ per-app composition roots — pos/ and order/
src/presentation/ handlers/{pos,order,hooks}, views/components/ (shared), views/screens/pos/ and views/screens/order/
api-contract/ OpenAPI spec (src/api.yaml) + generated TS and Go clients
provider/ App-level providers (Tamagui, theme, toast)
docs/ PRDs and TRDs (product and technical design docs)
docs-site/ VitePress feature documentation site
@gatherloop-pos/ui exports the shared domain/data/utils layer; @gatherloop-pos/ui/pos and
@gatherloop-pos/ui/order each export only their own app's composition roots, so a POS surface
cannot accidentally import the customer app's screens (or vice versa) — see
docs/trd-ui-presentation-split-by-app.md.
The three frontends share the same UI layer through Tamagui, which renders to both React DOM and React Native.
- Node.js 20+ and npm
- Go 1.24+
- A MySQL database
npm install
cp apps/api/.env.example apps/api/.env # DB credentials, JWT secret, CORS origins
cp apps/pos-web/.env.example apps/pos-web/.env.local
cp apps/order-web/.env.example apps/order-web/.env.local
cp apps/pos-mobile/.env.example apps/pos-mobile/.env
cp apps/kds-mobile/.env.example apps/kds-mobile/.envapps/api's checkout endpoint validates ORDER_PAYMENT_WALLET_ID on each request and returns an
error if it is unset, unknown, deleted, or not a payment target (docs/prd-order-checkout-qris-doku.md,
D15) — before trying checkout locally, create a wallet (POS → Wallets) and point apps/api/.env at
its id.
apps/api/.env's EXPO_PUSH_ACCESS_TOKEN, KDS_PUSH_SOUND and KDS_DISPATCH_INTERVAL_SECONDS
configure push delivery to apps/kds-mobile (docs/prd-kds-order-notifications.md) — the app
still registers and boots without a real Expo token, but no notification is actually delivered
until one is set.
apps/api/.env's CASH_PAYMENT_EXPIRY_SECONDS (default 600) is the walk-and-queue window an
order-app guest gets to pay cash at the till before the order is cancelled automatically
(docs/prd-order-cash-payment.md, D5). apps/order-web/.env.local's
NEXT_PUBLIC_ORDER_CASH_PAYMENT_ENABLED is the method's own kill switch, off by default —
NEXT_PUBLIC_ORDER_CHECKOUT_ENABLED must also be true for the cart's pay button to be reachable
at all; NEXT_PUBLIC_ORDER_CASHIER_LOCATION (default Lantai 1) is the till location shown to the
guest in both the checkout sheet and the cash instruction screen.
apps/api/.env's FONNTE_TOKEN, FONNTE_BASE_URL and ORDER_WEB_BASE_URL configure the
WhatsApp order-ready notification sent when a barista marks an order-app order ready
(docs/prd-order-whatsapp-notifications.md) — leaving FONNTE_TOKEN or ORDER_WEB_BASE_URL
empty (the default) boots the API with a disabled gateway that records every guest notification
skipped rather than failing checkout or the dispatcher, which is the expected state for local
dev, CI and the e2e stack. go run ./cmd/fonntecheck -to 0812… from apps/api sends one real
message through a configured gateway and prints Fonnte's raw response.
npx nx run api:serve # Go API, on the PORT set in apps/api/.env
npx nx run pos-web:dev # POS web app → http://localhost:3000
npx nx run order-web:dev # customer app → http://localhost:3000
npx nx run pos-mobile:start # React Native dev server (then run-android / run-ios)
npx nx run kds-mobile:start # React Native dev server (then run-android / run-ios)
npx nx run ui:storybook # component explorer → http://localhost:6006Web and order proxy /api/* to NEXT_PUBLIC_API_BASE_URL, so start the API first. Both default to
port 3000 — to run them side by side, give one another port (npx nx run order-web:dev --port=3001) and
add that origin to CORS_ALLOWED_ORIGINS in apps/api/.env.
npm test # all unit tests (Jest for TS, go test for the API)
npm run lint
npx nx run pos-web-e2e:e2e # Playwright end-to-end tests
npx nx run order-web-e2e:e2e # Playwright end-to-end tests (order app)
npx nx run api-contract:generate:ts # regenerate TS client after editing src/api.yaml
npx nx run api-contract:generate:go # regenerate Go modelsCodegen runs automatically as a dependency of the dev, build and serve targets — run it
manually only when you want to inspect the output.
In CI, a pull request runs the libs/ui and apps/api unit tests, and only the ones whose area it
touches (.github/workflows/pr-tests.yml). The end-to-end suites are too slow for that loop, so
they run after the merge, against a MySQL service, the real API binary and a real Next.js server
(.github/workflows/e2e-main.yml).
Both sides follow the same Clean Architecture split: domain → data → presentation, where the domain depends on nothing and the outer layers depend inwards through interfaces.
domain/ <entity|repository|usecase>.go per feature
data/mysql/, data/mock/ repository implementations
presentation/restapi/ handler + route + transformer per feature
main.go wires repositories → use cases → handlers
- Domain — entities, business logic in use cases, and repository interfaces. No SQL, no HTTP.
- Data — implements those interfaces against MySQL (or in-memory mocks used by tests).
- Presentation — HTTP handlers built on
gorilla/mux; transformers map between JSON request and response shapes and domain entities.
Dependencies are constructed once in main.go and injected, so use cases are testable in isolation
(see the *_usecase_test.go files next to them).
domain/ entities, repository interfaces, use cases (framework-agnostic)
data/ api/ (OpenAPI client), mock/, memory/, browser/ repository implementations
presentation/ handlers/ (+ handlers/hooks/), views/ (screens/, components/)
app/ per-route composition: builds repositories + use cases, renders a Handler
- Domain — each use case is a finite state machine: a state union (
idle,loading,loaded,error, …), an action union, and a pure reducer. Contains no React. - Data — implements the repository interfaces, mostly against the generated OpenAPI client with TanStack Query, and maps API types to entities. Swapping in a mock repository is how use cases and screens are tested.
- Presentation —
useUsecasebinds a use case's state machine to React (useReducer+ effects); a*Handlercalls it directly (or a shared hook inhandlers/hooks/when ≥2 handlers need the same one), maps that state to props withts-pattern; a*Screenis pure Tamagui JSX with Storybook stories. - app/ — the composition root: instantiates repositories and use cases, then renders the
handler. Split into
app/pos/andapp/order/; pages inapps/pos-web,apps/order-webandapps/pos-mobilemostly just re-export these.
Every surface builds with the React Compiler, so don't
hand-write useMemo/useCallback/React.memo for re-render performance — opt a misbehaving
component out with "use no memo" instead. (One exception: useCallback around a useFocusEffect
callback is still needed, because the Jest setup has no compiler pass.) See
docs/trd-react-compiler-adoption.md.
docs/— PRDs and TRDs for each feature; read the relevant one before changing behaviour.docs/forms.md— form conventions (react-hook-form + zod).docs/trd-vps-deployment-automation.md— how the API ships: a static binary built in CI and run on a VPS under systemd (.github/workflows/deploy-api.yml). The order app deploys to its own Vercel project (apps/order-web/vercel.json).docs/trd-storybook-vercel-deployment.md— how the component explorer ships: its own Vercel project rooted atlibs/ui(libs/ui/vercel.json).
