Simple POS System — Full project
This repository is a minimal, ready-to-run Point of Sale (POS) system built
with [Link] + Express + SQLite for the backend and a compact React
frontend. It is intended as a starting point you can run locally and extend.
Features
Product listing (CRUD endpoints in the API)
Add products to cart on the front end
Simple checkout that records a sale and sale_items in SQLite
Stock quantity adjustment on checkout
Basic search and quantity controls
Project structure (single-folder quick-start)
pos-project/
├─ server/
│ ├─ [Link]
│ ├─ [Link]
│ ├─ db_init.sql
│ └─ [Link] (created at runtime)
├─ client/
│ ├─ [Link]
│ ├─ public/[Link]
│ └─ src/[Link]
└─ [Link]
Backend — server/[Link]
// server/[Link]
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const bodyParser = require('body-parser');
const DB_PATH = [Link](__dirname, '[Link]');
const db = new [Link](DB_PATH);
const app = express();
[Link]([Link]());
// Allow CORS for local dev
[Link]((req, res, next)=>{
[Link]('Access-Control-Allow-Origin', '*');
[Link]('Access-Control-Allow-Methods',
'GET,POST,PUT,DELETE,OPTIONS');
[Link]('Access-Control-Allow-Headers', 'Content-Type');
next();
});
// Initialize tables if not exist
const initSql = `
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku TEXT UNIQUE,
name TEXT NOT NULL,
price REAL NOT NULL DEFAULT 0,
stock INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS sales (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT DEFAULT (datetime('now','localtime')),
total REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS sale_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sale_id INTEGER,
product_id INTEGER,
qty INTEGER,
price REAL,
FOREIGN KEY (sale_id) REFERENCES sales(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id)
);
`;
[Link](initSql, (err)=>{
if(err) [Link]('DB init error', err);
});
// Seed sample products if empty
[Link]('SELECT COUNT(*) as c FROM products', (err, row) => {
if(!err && row.c === 0){
const stmt = [Link]('INSERT INTO products
(sku,name,price,stock) VALUES (?,?,?,?)');
const sample = [
['P001','Plain Coffee',2.5,50],
['P002','Blue Bottle',5.0,30],
['P003','Sandwich',3.75,20]
];
for(const s of sample) [Link](s);
[Link]();
[Link]('Seeded sample products');
}
});
// API: GET /api/products
[Link]('/api/products', (req, res) => {
const q = [Link].q || '';
const sql = `SELECT * FROM products WHERE name LIKE ? OR sku LIKE ?
ORDER BY name`;
[Link](sql, [`%${q}%`,`%${q}%`], (err, rows)=>{
if(err) return [Link](500).json({error: [Link]});
[Link](rows);
});
});
// API: POST /api/products (create)
[Link]('/api/products', (req, res) => {
const {sku, name, price, stock} = [Link];
const sql = 'INSERT INTO products (sku,name,price,stock) VALUES
(?,?,?,?)';
[Link](sql, [sku,name,price,stock], function(err){
if(err) return [Link](400).json({error: [Link]});
[Link]('SELECT * FROM products WHERE id=?', [[Link]],
(e,row)=>{
[Link](row);
});
});
});
// API: PUT /api/products/:id (update)
[Link]('/api/products/:id', (req,res)=>{
const id = [Link];
const {sku,name,price,stock} = [Link];
[Link]('UPDATE products SET sku=?,name=?,price=?,stock=? WHERE
id=?', [sku,name,price,stock,id], function(err){
if(err) return [Link](400).json({error: [Link]});
[Link]('SELECT * FROM products WHERE id=?', [id],
(e,row)=>[Link](row));
});
});
// API: POST /api/checkout
// body: { items: [{product_id, qty, price}], total }
[Link]('/api/checkout', (req,res)=>{
const {items, total} = [Link];
if(!items ||  || [Link]===0) return
[Link](400).json({error:'No items'});
[Link](()=>{
[Link]('BEGIN TRANSACTION');
[Link]('INSERT INTO sales (total) VALUES (?)', [total],
function(err){
if(err){ [Link]('ROLLBACK'); return
[Link](500).json({error:[Link]}); }
const saleId = [Link];
const insertItem = [Link]('INSERT INTO sale_items
(sale_id,product_id,qty,price) VALUES (?,?,?,?)');
const updateStock = [Link]('UPDATE products SET stock =
stock - ? WHERE id = ?');
for(const it of items){
[Link]([saleId, it.product_id, [Link], [Link]]);
[Link]([[Link], it.product_id]);
}
[Link]();
[Link]();
[Link]('COMMIT', (cErr)=>{
if(cErr){ [Link]('ROLLBACK'); return
[Link](500).json({error:[Link]}); }
[Link]({sale_id: saleId});
});
});
});
});
// API: GET /api/sales (list recent sales)
[Link]('/api/sales', (req,res)=>{
[Link]('SELECT * FROM sales ORDER BY created_at DESC LIMIT 50',
(err,rows)=>{
if(err) return [Link](500).json({error:[Link]});
[Link](rows);
});
});
// Start server
const PORT = [Link] || 4000;
[Link](PORT, ()=>[Link]('Server listening on', PORT));
DB init SQL (server/db_init.sql)
-- db_init.sql
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku TEXT UNIQUE,
name TEXT NOT NULL,
price REAL NOT NULL DEFAULT 0,
stock INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS sales (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT DEFAULT (datetime('now','localtime')),
total REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS sale_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sale_id INTEGER,
product_id INTEGER,
qty INTEGER,
price REAL
);
Backend [Link] (server/[Link])
{
"name": "pos-server",
"version": "1.0.0",
"main": "[Link]",
"scripts": {
"start": "node [Link]"
},
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.18.2",
"sqlite3": "^5.1.6"
}
}
Frontend — React single-file client/src/[Link]
// client/src/[Link]
import React, {useEffect, useState} from 'react';
const API = (path)=>`[Link]
export default function App(){
const [products,setProducts] = useState([]);
const [cart,setCart] = useState([]);
const [q,setQ] = useState('');
useEffect(()=>{ fetchProducts(); }, []);
function fetchProducts(){
fetch(API('products?q=' + encodeURIComponent(q)))
.then(r=>[Link]()).then(setProducts).catch([Link]);
}
function addToCart(p){
const existing = [Link](c=>c.product_id===[Link]);
if(existing){
setCart([Link](c=> c.product_id===[Link] ? {...c, qty:
[Link]([Link], [Link]+1)} : c));
} else {
setCart([...cart, {product_id: [Link], name: [Link], price:
[Link], qty: 1, stock: [Link]}]);
}
}
function changeQty(product_id, qty){
setCart([Link](c=> c.product_id===product_id ? {...c, qty:
[Link](1, [Link]([Link], qty))} : c));
}
function removeItem(product_id){
setCart([Link](c=>c.product_id!==product_id)); }
function total(){ return [Link]((s,i)=> s + [Link] * [Link],
0).toFixed(2); }
function checkout(){
const items = [Link](i=> ({product_id: i.product_id, qty: [Link],
price: [Link]}));
fetch(API('checkout'), {
method: 'POST', headers: {'Content-Type':'application/json'},
body: [Link]({items, total: parseFloat(total())})
}).then(r=>[Link]()).then((res)=>{
alert('Sale recorded — ID: ' + res.sale_id);
setCart([]);
fetchProducts();
}).catch(err=>{ [Link](err); alert('Checkout failed'); });
}
return (
<div style={{fontFamily:'Arial', padding:20}}>
<h1>Simple POS</h1>
<div style={{display:'flex',gap:20}}>
<div style={{flex:2}}>
<div style={{marginBottom:10}}>
<input placeholder="Search products" value={q}
onChange={e=>setQ([Link])} />
<button onClick={fetchProducts}>Search</button>
</div>
<div>
<h3>Products</h3>
<table style={{width:'100%', borderCollapse:'collapse'}}>
<thead>
<tr><th>Name</th><th>Price</th><th>Stock</th><th></th></tr>
</thead>
<tbody>
{[Link](p=> (
<tr key={[Link]}>
<td>{[Link]}</td>
<td>{[Link](2)}</td>
<td>{[Link]}</td>
<td><button disabled={[Link]<=0}
onClick={()=>addToCart(p)}>Add</button></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div style={{flex:1, borderLeft:'1px solid #ddd',
paddingLeft:20}}>
<h3>Cart</h3>
{[Link]===0 ? <p>Cart empty</p> : (
<div>
{[Link](i=> (
<div key={i.product_id}
style={{display:'flex',justifyContent:'space-between',
marginBottom:8}}>
<div style={{flex:1}}>{[Link]} x
<input style={{width:50, marginLeft:8}}
type="number" value={[Link]} min={1} max={[Link]}
onChange={e=>changeQty(i.product_id,
parseInt([Link]||1))} />
</div>
<div style={{width:120, textAlign:'right'}}>
{([Link] * [Link]).toFixed(2)}
<button style={{marginLeft:8}}
onClick={()=>removeItem(i.product_id)}>Remove</button>
</div>
</div>
))}
<div style={{borderTop:'1px solid #eee', paddingTop:10,
marginTop:10}}>
<strong>Total: ₱{total()}</strong>
<div style={{marginTop:10}}>
<button onClick={checkout}>Checkout</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}
Frontend client/[Link]
{
"name": "pos-client",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vite": "^5.0.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"start": "vite preview --port 3000"
}
}
public/[Link] should be a minimal HTML wrapper for the Vite app.
How to run (local dev)
1. Clone/copy the project into pos-project.
2. Start backend:
cd pos-project/server
npm install
node [Link]
# or: npm start
server listens on port 4000
3. Start frontend:
cd pos-project/client
npm install
npm run dev
open [Link] (Vite default) or the address shown in
terminal.
Next steps & improvements
Add user authentication (cashier accounts)
Add receipts printing or PDF export
Add payments (cash, card) and payment records
Add product categories, discounts, inventory reports
Add barcode scanning support (use device camera or USB scanner)
Replace SQLite with PostgreSQL or MySQL for multi-user deployment
Add real-time updates with WebSockets if multiple terminals are used
If you’d like, I can:
produce a ZIP of the project you can download,
scaffold this as a GitHub repo with README and commit-ready files,
add user authentication and role permissions,
wire up receipts or a printable sales invoice.
Tell me which of those you’d like and I’ll modify the project accordingly.