Imported from pmgt-it-consultancy/pmgt-flow-suite (
AGENTS.md). Install upstream withnpx skills add pmgt-it-consultancy/pmgt-flow-suite. Copyright stays with the author.
AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Git Workflow
- Do not create or use git worktrees in this repository.
- For isolated implementation work, create a normal branch from
stagingormain.
Project Overview
A fullstack POS (Point of Sale) system for restaurant operations, built as a monorepo with web (Next.js 16) and mobile (React Native/Expo) frontends sharing a Convex backend. Features order management, product catalog with modifiers, table management, takeout workflows, discount/void processing, receipt printing, audit logging, and sales reporting.
Commands
# Install dependencies (uses pnpm)
pnpm install
# Run all apps in development (web, native, backend)
pnpm dev
# Type checking across all packages
pnpm typecheck
# Lint and format (uses Biome, not ESLint/Prettier)
pnpm lint
pnpm format
pnpm check # lint + format combined
# Build all packages
pnpm build
# Run backend tests (Vitest + convex-test)
cd packages/backend && pnpm vitest
cd packages/backend && pnpm vitest run # single run, no watch
# Per-app commands
cd apps/web && pnpm lint
cd apps/native && pnpm ios
cd apps/native && pnpm android
Architecture
Monorepo Structure
- apps/web — Next.js 16 App Router, Tailwind CSS v4, Radix UI components, React Hook Form + Zod
- apps/native — React Native 0.81 + Expo 54, Tamagui (UI/styling), React Navigation (bottom tabs + stack), Zustand for local state, Bluetooth ESC/POS receipt printing
- packages/backend — Convex backend (schema, queries, mutations, actions, tests)
- packages/shared — Shared utilities
Managed with Turborepo (turbo.json) and pnpm workspaces.
Data Flow
Both frontends import from @packages/backend and use Convex client hooks (useQuery, useMutation) for real-time data. Authentication uses @convex-dev/auth with Convex Auth tables.
Backend (Convex)
Schema in packages/backend/convex/schema.ts. Key domain tables: stores, products, categories, modifierGroups, modifierOptions, modifierGroupAssignments, orders, orderItems, orderItemModifiers, orderDiscounts, orderVoids, tables, roles, auditLogs, dailyReports, settings.
Function files organized by domain:
- orders.ts — Order lifecycle (create, add/remove items, void)
- checkout.ts — Payment settlement with tax calculation
- products.ts / categories.ts — Product catalog CRUD
- modifierGroups.ts / modifierOptions.ts / modifierAssignments.ts — Modifier system
- tables.ts — Dine-in table management
- discounts.ts — Senior/PWD/promo/manual discounts
- voids.ts — Order and item void processing
- reports.ts — Sales and daily reports
- auditLogs.ts — Audit trail
- users.ts / roles.ts — User management and RBAC
- stores.ts — Multi-store support
- lib/auth.ts — Auth helpers
- lib/permissions.ts — Permission checking
- lib/taxCalculations.ts — Philippine VAT calculations
Web App (apps/web/src)
app/(admin)/— Admin panel routes: dashboard, orders, products, categories, modifiers, tables, reports, audit-logs, users, storescomponents/— Reusable UI componentshooks/— Custom React hooksstores/— Zustand stores
Colocated Page Architecture
For complex pages, use colocated folders with underscore prefix (ignored by Next.js routing):
app/(admin)/stores/
├── page.tsx # Main page component (kept minimal)
├── _components/ # Page-specific components
│ ├── index.ts
│ ├── StoreFormDialog.tsx
│ └── StoresTable.tsx
├── _hooks/ # Page-specific hooks
│ ├── index.ts
│ └── useStoreMutations.ts
└── _stores/ # Page-specific Zustand stores
└── useStoreFormStore.ts
Rules:
- Keep
page.tsxminimal — it should compose components, not contain business logic - Use Zustand for client-side state management (form state, UI state, selections)
- Extract mutations/queries into custom hooks when they have complex logic
- Use barrel exports (
index.ts) in each folder - Prefix folders with
_so Next.js ignores them as routes
Native App (apps/native/src)
Feature-based organization under src/features/:
home/— Active orders listtables/— Table management with quick actionsorders/— Order entry with line items and modifierscheckout/— Payment processingtakeout/— Takeout order workfloworder-history/— Past orderssettings/— Printer settings (Bluetooth ESC/POS)shared/— Shared components, hooks, UI primitives
Native App Styling (Tamagui)
The native app uses Tamagui v2 RC with v5 config (@tamagui/config/v5). Config is in apps/native/tamagui.config.ts. Brand color: #0D87E1.
Config setup: Uses defaultConfig from @tamagui/config/v5 as base, with @tamagui/config/v5-reanimated for animations. Key overrides: onlyAllowShorthands: false (allows full prop names like backgroundColor instead of only bg), allowedStyleValues: false (allows any values, not just tokens), defaultPosition: "relative" (RN-friendly). Custom color tokens added for brand/gray/badge colors.
Critical Tamagui gotchas:
- NEVER import
createTamaguifrom@tamagui/core— always import from"tamagui". Mixingtamaguiand@tamagui/coreimports can create duplicate module instances where the config set by one isn't visible to the other, causing "Can't find Tamagui configuration" runtime errors. - Don't re-export non-UI components from
ui/index.tsbarrel file — importing shared components that themselves import fromui/creates require cycles that can cause uninitialized values at runtime. - Metro config (
metro.config.js) pins@tamagui/coreviaextraNodeModulesto prevent duplicate resolution, and addsmjsto source extensions. - Babel plugin is optional (build-time optimizer). If it causes issues, it can be removed — Tamagui works at runtime without it.
Layout: Use XStack (flex-row) and YStack (flex-column) from tamagui for layout containers. Use React Native primitives (TouchableOpacity, TextInput, FlatList, ScrollView, Modal, etc.) directly from react-native.
UI primitives in src/features/shared/components/ui/:
Text—styled(SizableText)withvariant(default/heading/subheading/muted/error/success) andsize(xs/sm/base/lg/xl/2xl/3xl). Note: size uses"base"not"md".Button— RNTouchableOpacitywithvariant(primary/secondary/outline/ghost/destructive/success) andsize(sm/md/lg)Badge—XStackwithvariantandsizepropsCard—YStackwithvariant(default/outlined/elevated)Input,Chip,IconButton,Modal,Separator
Styling rules:
- Apply styles as Tamagui props (
backgroundColor,padding,borderRadius, etc.) onXStack/YStack, not viaclassName - For custom UI components, use explicit prop interfaces (don't extend RN
ViewPropsand spread onto Tamagui components — causes type conflicts) - Colors use hex values directly (e.g.,
"#F3F4F6") or Tamagui tokens (e.g.,"$gray100")
Key Patterns
- Auth:
@convex-dev/authwith auth tables spread into schema;getUserId(ctx)extracts user identity - Store scoping: Most queries/mutations take
storeIdand useby_storeindexes - Tax model: Philippine VAT (12%) with vatable/non-vat/VAT-exempt classification; calculations in
lib/taxCalculations.ts - Modifier system: Groups assigned to products or categories via join table (
modifierGroupAssignments), with optional min/max override - Order snapshots: Product names and prices are snapshotted into order items at creation time
- Audit logging: Operations tracked in
auditLogstable with store, action, entity references
Convex Development Guidelines
Function Syntax
Always use object-based syntax with validators. Every function must have returns validator:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const myQuery = query({
args: { id: v.id("orders") },
returns: v.null(),
handler: async (ctx, args) => { ... }
});
Critical Rules
- Use
withIndex()instead offilter()for all database queries - Index names follow
by_field1_and_field2convention; query order must match index field order internalQuery/internalMutation/internalActionfor private functions;query/mutation/actionfor public API- Actions cannot use
ctx.db— call queries/mutations viactx.runQuery/ctx.runMutation - Add
"use node";at top of files using Node.js modules - Use
Id<'tableName'>andDoc<'tableName'>from./_generated/dataModelfor type safety - Function references:
api.orders.getOrder(public),internal.orders.getOrder(private)
Environment Variables
Required in Convex dashboard:
OPENAI_API_KEY— Optional, for AI summaries
Required in apps/web/.env.local:
NEXT_PUBLIC_CONVEX_URL
Required in apps/native/.env.local:
EXPO_PUBLIC_CONVEX_URL
UI Design Principles (POS)
This is a POS system used by restaurant staff. Every UI decision must prioritize efficiency:
- Use all available space — flex-fill layouts, no dead whitespace. Buttons and interactive elements should expand to fill their containers.
- Large touch targets — staff tap quickly and repeatedly. Buttons must be large enough to hit without precision.
- Glanceable data — clocks, stats, order counts must be readable at arm's length. Use large, bold font sizes for key numbers.
- Information density over aesthetics — pack useful info into every screen. Combine sections side-by-side (e.g. clock + stats in one row, buttons + order list side-by-side) rather than stacking vertically with margins.
Touch Target Sizing
| Element | Minimum | Recommended |
|---|---|---|
| Any tappable element | 44px | 48-56px |
| Quantity +/- buttons | 44px | 56x56px |
| Quick action buttons | 44px | 48-52px height |
| Primary action buttons | 48px | 56px height |
| Checkbox/radio rows | 48px | 56px height |
| Modal action buttons | 48px | Full-width, 18px padding |
Quantity Controls Pattern
Use colored background tints for increment/decrement buttons:
// Decrement button (red tint)
<TouchableOpacity style={{
width: 56, height: 56, borderRadius: 12,
backgroundColor: "#FEE2E2", // red-100
justifyContent: "center", alignItems: "center",
}}>
<Ionicons name="remove" size={28} color="#EF4444" />
</TouchableOpacity>
// Increment button (green tint)
<TouchableOpacity style={{
width: 56, height: 56, borderRadius: 12,
backgroundColor: "#DCFCE7", // green-100
justifyContent: "center", alignItems: "center",
}}>
<Ionicons name="add" size={28} color="#22C55E" />
</TouchableOpacity>
// Quantity display
<YStack backgroundColor="#F3F4F6" borderRadius={12} paddingVertical={12} paddingHorizontal={24}>
<Text style={{ fontSize: 28, fontWeight: "700" }}>{quantity}</Text>
</YStack>
Modal Layout Pattern (Sticky Footer)
For modals with scrollable content and fixed actions, use this structure:
<RNModal visible={visible} transparent animationType="slide">
<View style={{ flex: 1, justifyContent: "flex-end" }}>
<Pressable onPress={onClose} style={StyleSheet.absoluteFill} /> {/* Backdrop */}
<KeyboardAvoidingView behavior="padding" style={{ maxHeight: "92%", backgroundColor: "#FFF", borderTopRadius: 16 }}>
<View style={{ maxHeight: "100%" }}>
{/* Fixed Header */}
<XStack paddingHorizontal={20} paddingTop={20} paddingBottom={16} borderBottomWidth={1}>
...
</XStack>
{/* Scrollable Content - NO flex:1 on ScrollView style */}
<ScrollView contentContainerStyle={{ padding: 20 }}>
...
</ScrollView>
{/* Fixed Footer */}
<YStack paddingHorizontal={20} paddingTop={16} paddingBottom={24} borderTopWidth={1}>
...
</YStack>
</View>
</KeyboardAvoidingView>
</View>
</RNModal>
Critical: Do NOT use style={{ flex: 1 }} on ScrollView inside KeyboardAvoidingView — it causes the ScrollView to collapse.
Destructive/Cancel Button Pattern
Use outlined style with light red background for cancel/destructive secondary actions:
<TouchableOpacity style={{
backgroundColor: "#FEF2F2", // red-50
borderRadius: 10,
borderWidth: 1,
borderColor: "#FECACA", // red-200
paddingVertical: 14,
paddingHorizontal: 20,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
}}>
<Ionicons name="close-circle-outline" size={20} color="#DC2626" style={{ marginRight: 8 }} />
<Text style={{ color: "#DC2626", fontWeight: "600", fontSize: 15 }}>Cancel Order</Text>
</TouchableOpacity>
Color Conventions
| Purpose | Background | Border | Text/Icon |
|---|---|---|---|
| Primary action | #0D87E1 |
- | #FFFFFF |
| Success/Confirm | #22C55E |
- | #FFFFFF |
| Increment button | #DCFCE7 |
- | #22C55E |
| Decrement button | #FEE2E2 |
- | #EF4444 |
| Cancel/Destructive | #FEF2F2 |
#FECACA |
#DC2626 |
| Selected state | #DBEAFE |
#0D87E1 |
#0D87E1 |
| Disabled | #9CA3AF |
- | #FFFFFF |
| Neutral/Default | #F3F4F6 |
#E5E7EB |
#374151 |
Deployment
Web deploys to Vercel with custom build command that deploys Convex first:
cd ../../packages/backend && npx convex deploy --cmd 'cd ../../apps/web && turbo run build' --cmd-url-env-var-name NEXT_PUBLIC_CONVEX_URL