Desktop Tutorial

Build a Point of Sale App

Build a real-world Point of Sale system for a retail store. This advanced tutorial covers product catalog, sales transactions, customer management, receipt generation, inventory tracking, and daily reporting — all running as a native desktop application.

What We're Building

By the end of this tutorial you will have a fully functional POS desktop application that a small retail store can use day-to-day. The app runs as a single native binary with no browser, no internet dependency, and no external database server.

Product CatalogSKU, barcode, pricing, cost tracking, category grouping
Sales TransactionsCart-based checkout, multiple payment methods, tax calculation
Customer ManagementContact details, purchase history, loyalty points
Receipt PrintingPDF receipt generation from sale data
Inventory TrackingStock decrement on sale, low-stock alerts
Daily ReportsRevenue, expenses, profit, and top-selling products

Why a Desktop App?

A POS system needs to be fast, reliable, and work without an internet connection. Grit Desktop gives you all of that:

  • Offline-first — SQLite stores everything locally. No cloud database, no network latency, no downtime when the internet goes out.
  • Single binary — The Go backend, React frontend, and SQLite database all compile into one executable. Drop it on any machine and it runs.
  • Native performance — Go handles the business logic. No Electron overhead, no JVM, no interpreter.
  • Easy distribution — Hand a store owner a USB drive with a single .exe file. They double-click it and start selling.
This tutorial assumes you have completed the Getting Started guide and have Go, Node.js, and the Wails CLI installed.

Tech Stack

GoBackend logic, services, data layer
Wails v2Desktop runtime, Go-JS bridge
React + ViteFrontend UI with hot reload
TanStack RouterFile-based routing with type-safe params
SQLite (GORM)Embedded local database
Tailwind CSSUtility-first styling
Grit CLIScaffolding and code generation
1

Scaffold the Project

Create a new Grit Desktop project. This generates a complete Wails application with Go backend, React frontend, authentication, and two starter resources (Blog and Contact).

terminal
$ grit new-desktop pos-system
$ cd pos-system

Verify everything works by starting the dev server:

terminal
$ wails dev

You should see a native desktop window open with the default dashboard. Close it when you are satisfied it works — we have resources to generate.

2

Plan the Data Model

Before generating code, plan the resources. A POS system revolves around five core entities. Here is the complete data model we will generate:

ResourceFieldsPurpose
Productname, sku, price, cost, stock, category, barcodeProduct catalog with pricing and inventory
Customername, email, phone, address, loyalty_pointsCustomer records and loyalty program
Salecustomer_id, total, tax, payment_method, status, notesSales transactions
SaleItemsale_id, product_id, quantity, unit_price, subtotalIndividual line items within a sale
Expensedescription, amount, category, dateBusiness expenses for profit calculation

The Sale and SaleItem tables form a one-to-many relationship: each sale contains multiple line items. The SaleItem also references Product via product_id. These foreign keys are declared as uint fields — GORM handles the rest.

3

Generate All Resources

Run these five commands to generate the entire data layer, service layer, and frontend pages. Each command creates 4 new files and performs 10 code injections.

terminal
$ grit generate resource Product --fields "name:string,sku:string,price:float,cost:float,stock:int,category:string,barcode:string"
$ grit generate resource Customer --fields "name:string,email:string,phone:string,address:text,loyalty_points:int"
$ grit generate resource Sale --fields "customer_id:uint,total:float,tax:float,payment_method:string,status:string,notes:text"
$ grit generate resource SaleItem --fields "sale_id:uint,product_id:uint,quantity:int,unit_price:float,subtotal:float"
$ grit generate resource Expense --fields "description:string,amount:float,category:string,date:date"

After all five commands complete, here is what you have:

5GORM models
5Service files
15React routes
50Code injections
7Files modified
20New files total

The 50 injections span across db.go, main.go, app.go, types.go, cmd/studio/main.go, and sidebar.tsx. Every resource gets its own sidebar entry, route file, and bound methods automatically.

If you want to review what each injection does, see the Resource Generation reference page for the complete 10-marker table.
4

Explore the Generated Models

Open internal/models/product.go. Grit generated a complete GORM model with all seven fields, timestamps, and soft delete support:

internal/models/product.go
package models
import (
"time"
"gorm.io/gorm"
)
type Product struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `json:"name"`
Sku string `json:"sku"`
Price float64 `json:"price"`
Cost float64 `json:"cost"`
Stock int `json:"stock"`
Category string `json:"category"`
Barcode string `json:"barcode"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
}

Now look at the Sale model. Notice the customer_id field is generated as uint, which GORM treats as a foreign key. You can optionally add a relationship struct tag to enable eager loading:

internal/models/sale.go
package models
import (
"time"
"gorm.io/gorm"
)
type Sale struct {
ID uint `gorm:"primaryKey" json:"id"`
CustomerID uint `json:"customer_id"`
Total float64 `json:"total"`
Tax float64 `json:"tax"`
PaymentMethod string `json:"payment_method"`
Status string `json:"status"`
Notes string `json:"notes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
// Optional: add relationships for eager loading
// Customer Customer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"`
// Items []SaleItem `gorm:"foreignKey:SaleID" json:"items,omitempty"`
}

The SaleItem model has both sale_id and product_id as foreign keys, linking each line item to its parent sale and the product being sold. This is the backbone of the transaction system.

5

Customize the Product Service

The generated ProductService already has full CRUD methods (List, ListAll, GetByID, Create, Update, Delete). For a POS system, we need additional queries. Add a low-stock alert method and a barcode lookup:

internal/services/product.go
// GetLowStock returns products with stock below the given threshold.
func (s *ProductService) GetLowStock(threshold int) ([]models.Product, error) {
var products []models.Product
result := s.db.Where("stock < ?", threshold).Order("stock ASC").Find(&products)
return products, result.Error
}
// GetByBarcode looks up a single product by its barcode string.
func (s *ProductService) GetByBarcode(barcode string) (*models.Product, error) {
var product models.Product
result := s.db.Where("barcode = ?", barcode).First(&product)
if result.Error != nil {
return nil, result.Error
}
return &product, nil
}
// GetByCategory returns all products in a given category.
func (s *ProductService) GetByCategory(category string) ([]models.Product, error) {
var products []models.Product
result := s.db.Where("category = ?", category).Order("name ASC").Find(&products)
return products, result.Error
}
// DecrementStock reduces the stock of a product by the given quantity.
// Returns an error if insufficient stock is available.
func (s *ProductService) DecrementStock(productID uint, qty int) error {
return s.db.Model(&models.Product{}).
Where("id = ? AND stock >= ?", productID, qty).
Update("stock", gorm.Expr("stock - ?", qty)).Error
}

Now expose these new methods through the App struct in app.go. Wails automatically binds these as callable functions in the frontend:

app.go
// GetLowStockProducts returns products with stock below 10 units.
func (a *App) GetLowStockProducts() ([]models.Product, error) {
return a.product.GetLowStock(10)
}
// LookupBarcode finds a product by its barcode for the POS scanner.
func (a *App) LookupBarcode(barcode string) (*models.Product, error) {
return a.product.GetByBarcode(barcode)
}
// GetProductsByCategory returns all products in a given category.
func (a *App) GetProductsByCategory(category string) ([]models.Product, error) {
return a.product.GetByCategory(category)
}
6

Add Sale Processing Logic

This is the core of the POS system. When a cashier completes a sale, the app needs to do four things atomically:

  1. Create the Sale record with totals and tax
  2. Create a SaleItem for each product in the cart
  3. Decrement the stock on each Product
  4. Award loyalty points to the Customer (if any)

All four operations must succeed or fail together. GORM transactions make this straightforward. First, define the input type in internal/types/types.go:

internal/types/types.go
// ProcessSaleInput is the payload from the frontend checkout form.
type ProcessSaleInput struct {
CustomerID uint `json:"customer_id"`
PaymentMethod string `json:"payment_method"`
Notes string `json:"notes"`
TaxRate float64 `json:"tax_rate"`
Items []SaleItemInput `json:"items"`
}
// SaleItemInput represents a single line item in the cart.
type SaleItemInput struct {
ProductID uint `json:"product_id"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
}

Now add the ProcessSale method to app.go. This method wraps all four operations in a single database transaction:

app.go
// ProcessSale handles the complete checkout flow within a database
// transaction. It creates the sale, line items, updates stock, and
// awards loyalty points.
func (a *App) ProcessSale(input types.ProcessSaleInput) (*models.Sale, error) {
var sale models.Sale
err := a.db.Transaction(func(tx *gorm.DB) error {
// Calculate totals from line items
var subtotal float64
for _, item := range input.Items {
subtotal += item.UnitPrice * float64(item.Quantity)
}
tax := subtotal * input.TaxRate
total := subtotal + tax
// 1. Create the Sale record
sale = models.Sale{
CustomerID: input.CustomerID,
Total: total,
Tax: tax,
PaymentMethod: input.PaymentMethod,
Status: "completed",
Notes: input.Notes,
}
if err := tx.Create(&sale).Error; err != nil {
return fmt.Errorf("create sale: %w", err)
}
// 2. Create SaleItem records for each line item
for _, item := range input.Items {
saleItem := models.SaleItem{
SaleID: sale.ID,
ProductID: item.ProductID,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
Subtotal: item.UnitPrice * float64(item.Quantity),
}
if err := tx.Create(&saleItem).Error; err != nil {
return fmt.Errorf("create sale item: %w", err)
}
// 3. Decrement product stock
result := tx.Model(&models.Product{}).
Where("id = ? AND stock >= ?", item.ProductID, item.Quantity).
Update("stock", gorm.Expr("stock - ?", item.Quantity))
if result.Error != nil {
return fmt.Errorf("decrement stock: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("insufficient stock for product %d", item.ProductID)
}
}
// 4. Award loyalty points to the customer (1 point per dollar)
if input.CustomerID > 0 {
points := int(total)
err := tx.Model(&models.Customer{}).
Where("id = ?", input.CustomerID).
Update("loyalty_points", gorm.Expr("loyalty_points + ?", points)).Error
if err != nil {
return fmt.Errorf("update loyalty points: %w", err)
}
}
return nil
})
if err != nil {
return nil, err
}
return &sale, nil
}

If any step fails — for example, insufficient stock on a product — the entire transaction rolls back. No partial sales, no orphaned records, no inventory discrepancies. The gorm.Expr calls ensure the stock decrement and loyalty point update happen atomically at the database level.

The result.RowsAffected == 0 check is critical. It catches the case where a product has been sold out between when the cashier added it to the cart and when they hit checkout.
7

Customize the Frontend

The generated list and form pages are fully functional out of the box, but a POS system needs a checkout-oriented UI. Let us build a barcode lookup component and a Quick Sale page.

First, create a barcode search input. Wails generates TypeScript bindings for every bound Go method, so you can call LookupBarcode directly from React:

frontend/src/components/barcode-scanner.tsx
import { useState } from "react";
import { LookupBarcode } from "../../wailsjs/go/main/App";
interface Product {
id: number;
name: string;
price: number;
stock: number;
barcode: string;
}
interface BarcodeScannerProps {
onProductFound: (product: Product) => void;
}
export function BarcodeScanner({ onProductFound }: BarcodeScannerProps) {
const [barcode, setBarcode] = useState("");
const [error, setError] = useState("");
const handleScan = async () => {
if (!barcode.trim()) return;
setError("");
try {
const product = await LookupBarcode(barcode.trim());
if (product) {
onProductFound(product);
setBarcode("");
}
} catch (err) {
setError("Product not found");
}
};
return (
<div className="flex gap-2">
<input
type="text"
value={barcode}
onChange={(e) => setBarcode(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleScan()}
placeholder="Scan or type barcode..."
className="flex-1 px-3 py-2 rounded-md border bg-background"
autoFocus
/>
<button
onClick={handleScan}
className="px-4 py-2 rounded-md bg-primary text-primary-foreground"
>
Add
</button>
{error && <span className="text-destructive text-sm">{error}</span>}
</div>
);
}

Next, create the Quick Sale page. This page displays a running cart, lets the cashier scan barcodes, and processes the sale on checkout. Add this as a new page at frontend/src/pages/pos/index.tsx:

frontend/src/pages/pos/index.tsx
import { useState } from "react";
import { BarcodeScanner } from "../../components/barcode-scanner";
import { ProcessSale } from "../../../wailsjs/go/main/App";
interface CartItem {
product_id: number;
name: string;
quantity: number;
unit_price: number;
}
export default function POSPage() {
const [cart, setCart] = useState<CartItem[]>([]);
const [paymentMethod, setPaymentMethod] = useState("cash");
const TAX_RATE = 0.08; // 8% sales tax
const subtotal = cart.reduce(
(sum, item) => sum + item.unit_price * item.quantity, 0
);
const tax = subtotal * TAX_RATE;
const total = subtotal + tax;
const addToCart = (product: any) => {
setCart((prev) => {
const existing = prev.find((i) => i.product_id === product.id);
if (existing) {
return prev.map((i) =>
i.product_id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...prev, {
product_id: product.id,
name: product.name,
quantity: 1,
unit_price: product.price,
}];
});
};
const handleCheckout = async () => {
try {
await ProcessSale({
customer_id: 0,
payment_method: paymentMethod,
notes: "",
tax_rate: TAX_RATE,
items: cart.map((item) => ({
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price,
})),
});
setCart([]);
alert("Sale completed!");
} catch (err) {
alert("Error: " + err);
}
};
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Quick Sale</h1>
<BarcodeScanner onProductFound={addToCart} />
{/* Cart items table, totals, checkout button */}
{/* ... build out the full UI here */}
</div>
);
}

To register this page, create a route file at frontend/src/routes/_layout/pos.index.tsx and wrap the component with createFileRoute:

frontend/src/routes/_layout/pos.index.tsx
// frontend/src/routes/_layout/pos.index.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_layout/pos/")({
component: POSPage,
});
// ... POSPage component from above

Add a sidebar entry in sidebar.tsx so the cashier can navigate to the POS page. The generated sidebar uses Lucide icons — ShoppingCart is a good fit for the POS entry.

The generated product list page at /products still works for inventory management. The POS page is an additional view optimized for checkout speed.
8

Add Receipt Generation

After completing a sale, the store needs a receipt. Go makes it easy to generate a text-based receipt that can be saved as a file or sent to a thermal printer. Add this method to app.go:

app.go
// GenerateReceipt builds a plain-text receipt for a completed sale.
// Returns the receipt string for display or printing.
func (a *App) GenerateReceipt(saleID uint) (string, error) {
// Fetch the sale
var sale models.Sale
if err := a.db.First(&sale, saleID).Error; err != nil {
return "", fmt.Errorf("sale not found: %w", err)
}
// Fetch the line items
var items []models.SaleItem
if err := a.db.Where("sale_id = ?", saleID).Find(&items).Error; err != nil {
return "", fmt.Errorf("fetch items: %w", err)
}
// Fetch product names for each item
type lineItem struct {
Name string
Qty int
Price float64
Subtotal float64
}
var lines []lineItem
for _, item := range items {
var product models.Product
a.db.First(&product, item.ProductID)
lines = append(lines, lineItem{
Name: product.Name,
Qty: item.Quantity,
Price: item.UnitPrice,
Subtotal: item.Subtotal,
})
}
// Fetch customer name if available
customerName := "Walk-in Customer"
if sale.CustomerID > 0 {
var customer models.Customer
if err := a.db.First(&customer, sale.CustomerID).Error; err == nil {
customerName = customer.Name
}
}
// Build the receipt
var b strings.Builder
b.WriteString("========================================\n")
b.WriteString(" POS SYSTEM RECEIPT \n")
b.WriteString("========================================\n")
b.WriteString(fmt.Sprintf("Date: %s\n", sale.CreatedAt.Format("2006-01-02 15:04")))
b.WriteString(fmt.Sprintf("Sale #: %d\n", sale.ID))
b.WriteString(fmt.Sprintf("Customer: %s\n", customerName))
b.WriteString(fmt.Sprintf("Payment: %s\n", sale.PaymentMethod))
b.WriteString("----------------------------------------\n")
b.WriteString(fmt.Sprintf("%-20s %5s %10s\n", "Item", "Qty", "Amount"))
b.WriteString("----------------------------------------\n")
for _, line := range lines {
b.WriteString(fmt.Sprintf("%-20s %5d %10.2f\n", line.Name, line.Qty, line.Subtotal))
}
b.WriteString("----------------------------------------\n")
b.WriteString(fmt.Sprintf("%-26s %10.2f\n", "Subtotal:", sale.Total-sale.Tax))
b.WriteString(fmt.Sprintf("%-26s %10.2f\n", "Tax:", sale.Tax))
b.WriteString(fmt.Sprintf("%-26s %10.2f\n", "TOTAL:", sale.Total))
b.WriteString("========================================\n")
b.WriteString(" Thank you for your purchase! \n")
b.WriteString("========================================\n")
return b.String(), nil
}

On the frontend, call GenerateReceipt after a successful sale and display it in a modal or print dialog:

frontend/src/pages/pos/index.tsx
import { GenerateReceipt } from "../../../wailsjs/go/main/App";
// After successful ProcessSale call:
const receipt = await GenerateReceipt(sale.id);
// Display in a <pre> block or open the system print dialog

For thermal printers, the plain-text format works directly. Most receipt printers accept raw text via their driver. For PDF export, you can use a Go library like jung-kurt/gofpdf or signintech/gopdf to convert the receipt into a downloadable PDF.

9

Daily Reports

A store owner needs to see how the day went. Add a report method that aggregates sales, expenses, and profit for a given date. First, define the report struct in internal/types/types.go:

internal/types/types.go
// DailyReport aggregates the financial data for a single day.
type DailyReport struct {
Date string `json:"date"`
TotalSales float64 `json:"total_sales"`
TotalTax float64 `json:"total_tax"`
TotalExpenses float64 `json:"total_expenses"`
NetProfit float64 `json:"net_profit"`
TransactionCount int `json:"transaction_count"`
TopProducts []TopProduct `json:"top_products"`
}
// TopProduct represents a best-selling product for the report.
type TopProduct struct {
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
QuantitySold int `json:"quantity_sold"`
Revenue float64 `json:"revenue"`
}

Now add the report method to app.go. It runs three queries: one for sales totals, one for expenses, and one for the top-selling products:

app.go
// GetDailyReport returns aggregated sales, expenses, and profit
// for a specific date (format: "2006-01-02").
func (a *App) GetDailyReport(date string) (*types.DailyReport, error) {
report := &types.DailyReport{Date: date}
// Parse the date to get start and end boundaries
start, err := time.Parse("2006-01-02", date)
if err != nil {
return nil, fmt.Errorf("invalid date format: %w", err)
}
end := start.Add(24 * time.Hour)
// Aggregate sales
var salesResult struct {
Total float64
Tax float64
Count int64
}
a.db.Model(&models.Sale{}).
Where("created_at >= ? AND created_at < ? AND status = ?", start, end, "completed").
Select("COALESCE(SUM(total), 0) as total, COALESCE(SUM(tax), 0) as tax, COUNT(*) as count").
Scan(&salesResult)
report.TotalSales = salesResult.Total
report.TotalTax = salesResult.Tax
report.TransactionCount = int(salesResult.Count)
// Aggregate expenses
var expenseTotal float64
a.db.Model(&models.Expense{}).
Where("date >= ? AND date < ?", start, end).
Select("COALESCE(SUM(amount), 0)").
Scan(&expenseTotal)
report.TotalExpenses = expenseTotal
report.NetProfit = report.TotalSales - report.TotalTax - report.TotalExpenses
// Top 5 selling products
var topProducts []types.TopProduct
a.db.Model(&models.SaleItem{}).
Select("sale_items.product_id, products.name as product_name, SUM(sale_items.quantity) as quantity_sold, SUM(sale_items.subtotal) as revenue").
Joins("JOIN products ON products.id = sale_items.product_id").
Joins("JOIN sales ON sales.id = sale_items.sale_id").
Where("sales.created_at >= ? AND sales.created_at < ? AND sales.status = ?", start, end, "completed").
Group("sale_items.product_id, products.name").
Order("quantity_sold DESC").
Limit(5).
Scan(&topProducts)
report.TopProducts = topProducts
return report, nil
}

The frontend can call GetDailyReport("2026-03-04") and render the results in a dashboard with stats cards and a top-products table. The generated DataTable component works perfectly for displaying the top products list.

For weekly or monthly reports, follow the same pattern but adjust the date range calculation. You could add GetWeeklyReport and GetMonthlyReport methods that call GetDailyReport across a range and aggregate the results.
10

Build and Distribute

Your POS system is feature-complete. Compile it into a native executable:

terminal
$ grit compile

This runs wails build under the hood and produces a single binary in build/bin/. The binary includes everything:

Go BackendAll services, models, and business logic compiled into machine code
React FrontendThe entire UI bundled via Vite and embedded with go:embed
SQLite EngineDatabase engine statically linked -- no external DB server needed
Wails RuntimeNative window management and Go-JavaScript bridge

Build for a specific platform if you need cross-platform distribution:

terminal
# Windows (.exe)
$ wails build -platform windows/amd64
# macOS (Apple Silicon)
$ wails build -platform darwin/arm64
# Linux
$ wails build -platform linux/amd64

For Windows, you can also build an NSIS installer for a professional installation experience:

terminal
$ wails build -nsis

Distribution Checklist

TaskDetails
App iconPlace a 1024x1024 PNG at build/appicon.png before building
Database locationSQLite creates app.db in the working directory by default
No internet requiredEverything is local -- the app works fully offline
Data backupUsers can back up by copying the app.db file
UpdatesDistribute new versions by replacing the executable
Windows WebView2Required on Windows -- included in Windows 11, installable on Windows 10

That is it. Hand the store owner a USB drive with the .exe file. They double-click it, and they have a full POS system with product management, barcode scanning, checkout, receipts, and daily reports. No installation wizard, no database setup, no cloud account required.

Complete Project Structure

Here is the final project structure after all five resources have been generated and the custom POS features added:

pos-system/
pos-system/
├── main.go # Wails entry point
├── app.go # App struct + ProcessSale, reports, receipts
├── wails.json # Wails project configuration
├── go.mod / go.sum
├── internal/
│ ├── config/
│ │ └── config.go # App configuration
│ ├── db/
│ │ └── db.go # GORM setup + AutoMigrate (7 models)
│ ├── models/
│ │ ├── user.go # Auth user
│ │ ├── product.go # Product catalog
│ │ ├── customer.go # Customer records
│ │ ├── sale.go # Sales transactions
│ │ ├── sale_item.go # Sale line items
│ │ └── expense.go # Business expenses
│ ├── services/
│ │ ├── auth.go # Authentication
│ │ ├── product.go # Product CRUD + barcode lookup
│ │ ├── customer.go # Customer CRUD
│ │ ├── sale.go # Sale CRUD
│ │ ├── sale_item.go # SaleItem CRUD
│ │ └── expense.go # Expense CRUD
│ └── types/
│ └── types.go # Input structs + DailyReport
├── frontend/
│ └── src/
│ ├── components/
│ │ ├── sidebar.tsx # Navigation with all resources
│ │ └── barcode-scanner.tsx # Barcode input component
│ ├── routes/
│ │ ├── __root.tsx # Root route
│ │ ├── _layout.tsx # Auth guard + sidebar
│ │ └── _layout/ # Protected routes
│ │ ├── products.index.tsx
│ │ ├── customers.index.tsx
│ │ ├── sales.index.tsx
│ │ ├── sale-items.index.tsx
│ │ ├── expenses.index.tsx
│ │ └── pos.index.tsx # Quick Sale checkout
└── cmd/
└── studio/
└── main.go # GORM Studio (7 models registered)

Potential Enhancements

The POS system built in this tutorial is production-ready for a single store. Here are ideas for taking it further:

Barcode Scanner IntegrationUSB barcode scanners emit keystrokes. The barcode input field already handles this -- the scanner types the barcode and presses Enter. No additional driver or library needed.
Thermal Printer SupportUse Go's os/exec package to send the receipt text directly to a printer. On Windows, use the net use command to map a USB printer; on Linux, pipe to /dev/usb/lp0.
Multi-Store SyncAdd an optional HTTP sync endpoint. Each store runs locally with SQLite, then syncs transactions to a central PostgreSQL server when connected to the internet.
Discount SystemAdd a Discount model with percentage or fixed amount fields. Apply discounts per item or per sale in the ProcessSale transaction.
Cash Drawer ManagementTrack opening float, cash in/out, and end-of-day reconciliation. Add a CashDrawer model with shift-based records.
Product VariantsAdd a Variant model linked to Product for size, color, or other attributes. Each variant has its own SKU, barcode, and stock count.