Database Guide¶
Overview¶
The HOMEPOT system uses PostgreSQL for production-ready performance and scalability. This guide covers database setup, management, and usage patterns for the POS management system.
Migration Note: HOMEPOT previously supported SQLite but has migrated to PostgreSQL for better async support, improved performance, and production readiness. See PostgreSQL Migration Guide for details.
Database Structure¶
Core Tables¶
The HOMEPOT database consists of the following main entities:
sites- Restaurant locations and chain managementdevices- POS terminals and hardware inventoryjobs- Configuration update and management taskshealth_checks- Device monitoring and status trackingaudit_logs- System events and compliance trackingusers- System users and authentication
Entity Relationships¶
Sites (1) ←→ (Many) Devices
Sites (1) ←→ (Many) Jobs
Devices (1) ←→ (Many) Health Checks
Jobs (1) ←→ (Many) Audit Logs
Quick Start¶
Development Setup¶
-
Clone and Navigate
-
Install PostgreSQL
-
Initialize Database
This creates: - PostgreSQL database homepot_db - 3 demo sites - 12 POS terminals - User authentication tables
- Test Connection
Database Organization¶
File Structure¶
homepot-client/
├── backend/
│ ├── .env # Database connection string
│ ├── .env.example # Example configuration
│ └── src/homepot/
│ ├── database.py # Async database connection
│ ├── models.py # SQLAlchemy models
│ ├── config.py # Configuration management
│ └── app/db/database.py # Sync database for API v1
└── scripts/
└── init-postgresql.sh # Database initialization
Configuration¶
Database connections are configured in backend/.env:
# PostgreSQL (Production & Development)
DATABASE__URL=postgresql://homepot_user:homepot_dev_password@localhost:5432/homepot_db
# For testing, tests use in-memory SQLite databases automatically
# No TEST_DATABASE_URL configuration needed
Database Management¶
Common Operations¶
View Database Contents¶
# Using psql command line
export PGPASSWORD='homepot_dev_password'
psql -h localhost -U homepot_user -d homepot_db
# List all tables
\dt
# Show table structure
\d sites
# Run queries
SELECT * FROM sites;
# Exit
\q
Backup Database¶
# Create SQL dump
export PGPASSWORD='homepot_dev_password'
pg_dump -h localhost -U homepot_user homepot_db > backup_$(date +%Y%m%d).sql
# Restore from backup
psql -h localhost -U homepot_user -d homepot_db < backup.sql
Reset to Demo Data¶
Environment-Specific Databases¶
Development¶
- Type: PostgreSQL
- Database:
homepot_db - Location:
/var/lib/postgresql/16/main/ - Purpose: Demo data and development testing
- Contains: 3 sites, 12 devices, 1 test user
Testing¶
- Type: SQLite (in-memory, temporary)
- Purpose: Automated test isolation
- Contains: Clean test data, created and destroyed per test run
Production¶
- Type: PostgreSQL
- Purpose: Live deployment
- Configuration: Via
DATABASE__URLenvironment variable
Testing¶
Database Testing Strategy¶
# Test database isolation
import pytest
from src.homepot.database import get_test_db
@pytest.fixture
def db_session():
"""Provide clean database session for each test."""
db = get_test_db()
yield db
db.rollback() # Clean up after test
def test_site_creation(db_session):
"""Test creating a new site."""
site = Site(name="test-site", location="Test City")
db_session.add(site)
db_session.commit()
assert site.id is not None
assert site.name == "test-site"
Running Database Tests¶
# Run all database tests
pytest backend/tests/test_database.py -v
# Run with database reset
pytest backend/tests/ --reset-db
# Test specific functionality
pytest backend/tests/test_models.py::test_site_device_relationship
Demo Data Overview¶
The included demo database provides realistic test data:
Sites (14 locations)¶
- Restaurant chain locations:
site-123,site-456, etc. - Distributed across different regions
- Each site configured with multiple POS terminals
Devices (30 POS terminals)¶
- 5 terminals per site on average
- Named pattern:
pos-terminal-1throughpos-terminal-5 - Various status states for testing
Jobs (13 recent jobs)¶
- Payment configuration updates
- Software deployment tasks
- Job status progression:
queued→sent→acknowledged→completed
Health Checks (68 records)¶
- Regular device monitoring data
- Performance metrics and status
- Demonstrates monitoring capabilities
Advanced Usage¶
Adding More Sites and Devices¶
As you test the platform, you may want to add more sites and devices beyond the initial demo data (2 sites, 8 devices). There are several ways to do this:
Method 1: Using the REST API (Recommended)¶
The easiest way to add data is through the REST API when the server is running.
Start the server:
Add a new site:
curl -X POST "http://localhost:8000/sites" \
-H "Content-Type: application/json" \
-d '{
"site_id": "site-003",
"name": "North Branch",
"description": "Northern retail location with 4 POS terminals",
"location": "789 North Ave, North Side"
}'
Add devices to the site:
# First, get the site's internal ID from the response above or query sites
curl http://localhost:8000/sites
# Then add devices (replace {site_internal_id} with actual ID, e.g., 3)
curl -X POST "http://localhost:8000/devices" \
-H "Content-Type: application/json" \
-d '{
"device_id": "pos-terminal-009",
"name": "POS Terminal 9",
"device_type": "pos_terminal",
"site_id": 3,
"ip_address": "192.168.3.9",
"mac_address": "00:1B:44:11:3A:B9",
"config": {
"gateway_url": "https://payments.example.com"
}
}'
Verify:
curl http://localhost:8000/sites | python3 -m json.tool
curl http://localhost:8000/devices | python3 -m json.tool
Method 2: Using Python Script¶
Create a script to bulk-add data:
# scripts/add_bulk_data.py
import asyncio
import sys
import os
sys.path.insert(0, os.path.join(os.getcwd(), "backend"))
from homepot.database import DatabaseService
from homepot.models import DeviceType
async def add_bulk_data():
"""Add multiple sites and devices."""
db_service = DatabaseService()
# Add 3 new sites
sites_data = [
{
"site_id": "site-003",
"name": "North Branch",
"description": "Northern retail location",
"location": "789 North Ave"
},
{
"site_id": "site-004",
"name": "South Branch",
"description": "Southern retail location",
"location": "321 South St"
},
{
"site_id": "site-005",
"name": "East Branch",
"description": "Eastern retail location",
"location": "555 East Blvd"
}
]
created_sites = []
for site_data in sites_data:
site = await db_service.create_site(**site_data)
created_sites.append(site)
print(f"Created site: {site.name} (ID: {site.id})")
# Add devices to each new site
device_counter = 9 # Start after existing 8 devices
for site in created_sites:
for i in range(1, 5): # 4 devices per site
device = await db_service.create_device(
device_id=f"pos-terminal-{device_counter:03d}",
name=f"POS Terminal {device_counter}",
device_type=DeviceType.POS_TERMINAL,
site_id=site.id,
ip_address=f"192.168.{site.id}.{10+i}",
config={"gateway_url": "https://payments.example.com"}
)
print(f" Created device: {device.name}")
device_counter += 1
print(f"\nSuccessfully added {len(created_sites)} sites and {(device_counter-9)} devices!")
if __name__ == "__main__":
asyncio.run(add_bulk_data())
Run the script:
Method 3: Modify init-postgresql.sh¶
For permanent demo data changes, edit the initialization script:
# Edit scripts/init-postgresql.sh
nano scripts/init-postgresql.sh
# Find the section that creates demo sites and devices
# Add your custom sites/devices there
# Then recreate the database
./scripts/init-postgresql.sh
Method 4: Direct SQL (Advanced)¶
For quick testing, you can insert data directly:
-- Add a new site
INSERT INTO sites (site_id, name, description, location, created_at, updated_at)
VALUES ('site-006', 'Quick Test Site', 'Test location', '123 Test St',
datetime('now'), datetime('now'));
-- Get the site's internal ID
SELECT id FROM sites WHERE site_id = 'site-006';
-- Add a device (replace {site_internal_id} with actual ID)
INSERT INTO devices (device_id, name, device_type, site_id, ip_address,
status, created_at, updated_at)
VALUES ('pos-terminal-020', 'Test Terminal', 'pos_terminal',
6, '192.168.6.10', 'online', datetime('now'), datetime('now'));
-- Verify
SELECT s.name, COUNT(d.id) as device_count
FROM sites s
LEFT JOIN devices d ON s.id = d.site_id
GROUP BY s.id;
.quit
Example: Create 10 Sites with 50 Devices¶
# scripts/create_large_dataset.py
import asyncio
import sys
import os
sys.path.insert(0, os.path.join(os.getcwd(), "backend"))
from homepot.database import DatabaseService
from homepot.models import DeviceType
async def create_large_dataset():
"""Create 10 sites with 5 devices each (50 devices total)."""
db_service = DatabaseService()
device_counter = 1
for site_num in range(1, 11): # 10 sites
# Create site
site = await db_service.create_site(
site_id=f"site-{site_num:03d}",
name=f"Store Location {site_num}",
description=f"Retail store #{site_num} with 5 POS terminals",
location=f"{site_num * 100} Main Street, City {site_num}"
)
print(f"Created site: {site.name}")
# Create 5 devices per site
for device_num in range(1, 6):
device = await db_service.create_device(
device_id=f"pos-terminal-{device_counter:03d}",
name=f"POS Terminal {device_counter}",
device_type=DeviceType.POS_TERMINAL,
site_id=site.id,
ip_address=f"192.168.{site_num}.{10+device_num}",
mac_address=f"00:1B:44:11:{site_num:02X}:{device_num:02X}",
config={
"gateway_url": "https://payments.example.com",
"store_id": f"store-{site_num:03d}",
"terminal_id": f"term-{device_counter:03d}"
}
)
print(f" Created device: {device.name}")
device_counter += 1
print(f"\nSuccessfully created 10 sites and 50 devices!")
print(f"Total in database: {site_num} sites, {device_counter-1} devices")
if __name__ == "__main__":
asyncio.run(create_large_dataset())
Run it:
Interactive API Documentation¶
Use the built-in Swagger UI for interactive data creation:
-
Start the server:
-
Open browser: http://localhost:8000/docs
-
Create sites and devices interactively:
- Click on
POST /sitesendpoint - Click "Try it out"
- Enter site data in JSON format
- Click "Execute"
- Copy the returned site
idfor device creation - Repeat for
POST /devices
Verify Your Data¶
After adding data, verify the counts:
# Quick check via API
curl http://localhost:8000/sites | python3 -m json.tool | grep -c "site_id"
curl http://localhost:8000/devices | python3 -m json.tool | grep -c "device_id"
# Detailed check with psql
export PGPASSWORD='homepot_dev_password'
psql -h localhost -U homepot_user -d homepot_db << 'EOF'
SELECT
COUNT(DISTINCT s.id) as total_sites,
COUNT(d.id) as total_devices,
ROUND(AVG(device_count)::numeric, 2) as avg_devices_per_site
FROM sites s
LEFT JOIN devices d ON s.id = d.site_id
LEFT JOIN (
SELECT site_id, COUNT(*) as device_count
FROM devices
GROUP BY site_id
) dc ON s.id = dc.site_id;
EOF
User Management¶
Creating Test Users¶
Users can be created through the API or directly in the database. For testing, you can create users with Python:
Method 1: Using Python Script¶
Create a file create_user.py:
import asyncio
import sys
from pathlib import Path
import bcrypt
# Setup path
backend_dir = Path(__file__).parent / "backend"
sys.path.insert(0, str(backend_dir / "src"))
from homepot.database import DatabaseService
from homepot.models import User
async def create_user(username, email, password, is_admin=False):
"""Create a user with hashed password."""
# Hash password with bcrypt
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
# Create user in database
db = DatabaseService()
try:
async with db.get_session() as session:
from sqlalchemy import select
# Check if user exists
result = await session.execute(
select(User).where(User.email == email)
)
if result.scalar_one_or_none():
print(f"User {email} already exists!")
return
# Create new user
new_user = User(
username=username,
email=email,
hashed_password=hashed_password,
is_admin=is_admin,
is_active=True
)
session.add(new_user)
await session.commit()
print(f"Created user: {username} ({email})")
finally:
await db.close()
# Run from project root
asyncio.run(create_user("testuser", "test@homepot.com", "Test123!", is_admin=True))
Run it:
Method 2: Using API Signup Endpoint¶
curl -X POST http://localhost:8000/api/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "test@homepot.com",
"password": "Test123!",
"is_admin": true
}'
Method 3: Direct SQL (Not Recommended)¶
Warning: Direct SQL bypasses password hashing. Only use for understanding schema.
-- View users
SELECT id, username, email, is_admin, is_active, created_at
FROM users;
-- Check password hash format
SELECT id, username, LEFT(hashed_password, 30) as hash_sample
FROM users;
Viewing Users¶
# Using psql
export PGPASSWORD='homepot_dev_password'
psql -h localhost -U homepot_user -d homepot_db -c "
SELECT id, username, email, is_admin, is_active, created_at
FROM users
ORDER BY created_at DESC;
"
# Using query helper script
./scripts/query-db.sh users
Password Security¶
HOMEPOT uses bcrypt for password hashing:
- Hashed passwords are stored (e.g.,
$2b$12$gC6leMKvDICuZ...) - Never stored in plain text
- Cannot be reversed to see original password
- Secure even if database is compromised
Example hashed password structure:
$2b$12$gC6leMKvDICuZ6IsAxypFuelJgy8B85ropxHhOFpsJR...
│ │ │
│ │ └─ Salt (22 chars)
│ └──── Cost factor (2^12 iterations)
└──────── Algorithm (bcrypt)
Custom Queries¶
from src.homepot.database import get_db
from src.homepot.models import Site, Device
# Get all sites with device counts
db = get_db()
sites_with_devices = (
db.query(Site)
.join(Device)
.group_by(Site.id)
.all()
)
# Find inactive devices
inactive_devices = (
db.query(Device)
.filter(Device.status == 'offline')
.all()
)
Database Migrations (Future)¶
When schema changes are needed:
# Create migration
alembic revision --autogenerate -m "Add new table"
# Apply migration
alembic upgrade head
# Rollback if needed
alembic downgrade -1
Docker Usage¶
Database Persistence¶
# docker-compose.yml
services:
db:
image: postgres:16
environment:
- POSTGRES_DB=homepot
- POSTGRES_USER=homepot_user
- POSTGRES_PASSWORD=homepot_dev_password
volumes:
- postgres_data:/var/lib/postgresql/data
homepot-api:
depends_on:
- db
environment:
- DATABASE__URL=postgresql://homepot_user:homepot_dev_password@db:5432/homepot
volumes:
postgres_data:
Using PostgreSQL in Docker¶
# Start PostgreSQL container
docker-compose up -d db
# Access database
docker-compose exec db psql -U homepot_user -d homepot
# View logs
docker-compose logs db
Security Considerations¶
Development¶
- Use local PostgreSQL with development credentials
- Development database accessible only on localhost
- Demo data is non-sensitive
Production¶
- Use strong passwords for PostgreSQL
- Enable SSL/TLS for database connections
- Regular backups and monitoring
- Network-level access controls
- Environment-based configuration
Best Practices¶
# Always use environment variables for sensitive data
DATABASE__URL = os.getenv("DATABASE__URL") # From .env file
Production PostgreSQL¶
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: homepot
POSTGRES_USER: homepot_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
homepot-client:
environment:
- DATABASE_URL=postgresql://homepot_user:${POSTGRES_PASSWORD}@postgres:5432/homepot
Security Considerations¶
Development¶
- Use local PostgreSQL with development credentials
- Development database accessible only on localhost
- Demo data is non-sensitive
Production¶
- Use PostgreSQL with proper authentication
- Enable SSL/TLS for database connections
- Regular backups and monitoring
- Environment-based configuration
Best Practices¶
# Always use environment variables for sensitive data
from homepot.config import get_settings
settings = get_settings()
db_url = settings.database.url # From DATABASE__URL
# Use connection pooling for production
engine = create_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20,
pool_pre_ping=True
)
Related Documentation¶
- Getting Started - Initial setup and configuration
- Development Guide - Development workflow
- Deployment Guide - Production deployment
- POS Management - Device and site management
Troubleshooting¶
Common Issues¶
Cannot connect to database:
# Check if PostgreSQL is running
sudo systemctl status postgresql
# Restart PostgreSQL if needed
sudo systemctl restart postgresql
# Verify connection settings
psql -h localhost -U homepot_user -d homepot_db
Database doesn't exist:
Permission errors:
# Solution: Verify PostgreSQL user permissions
sudo -u postgres psql -c "\du homepot_user"
# Re-grant permissions if needed
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE homepot_db TO homepot_user;"
Performance Tips¶
- Use indexes for frequently queried fields
- Monitor query performance with
EXPLAIN ANALYZE - Consider connection pooling for high-load scenarios (already configured in SQLAlchemy)
- Regular database maintenance:
VACUUM ANALYZE - Monitor PostgreSQL logs:
/var/log/postgresql/
Scalability Patterns¶
To handle large datasets efficiently, the system implements the following patterns:
Pagination for Large Result Sets¶
When retrieving large numbers of records (e.g., devices for a site), use pagination to avoid memory issues. The DatabaseService provides methods like get_devices_by_site_and_segment_paginated which utilize LIMIT and OFFSET at the database level.
# Example of paginated retrieval
batch_size = 50
offset = 0
while True:
devices = await db_service.get_devices_by_site_and_segment_paginated(
site_id="site-123",
limit=batch_size,
offset=offset
)
if not devices:
break
# Process batch...
offset += batch_size
Batch Processing¶
Operations that affect many rows should be batched to manage transaction size and lock duration. The job orchestrator uses this pattern for sending push notifications to large device segments.
Support¶
For database-related issues: 1. Check PostgreSQL logs: /var/log/postgresql/postgresql-16-main.log 2. Verify database configuration in backend/.env 3. Test with a fresh database: ./scripts/init-postgresql.sh 4. Review configuration settings 5. Check environment variables
This guide covers the essential database operations for HOMEPOT. For advanced topics or specific issues, refer to the individual component documentation or create an issue in the project repository.