# Database Architecture **Version**: 0.12.20 **Last Updated**: 2026-01-28 Flyer Crawler uses PostgreSQL 16 with PostGIS for geographic data, pg_trgm for fuzzy text search, and uuid-ossp for UUID generation. The database contains 65 tables organized into logical domains. ## Table of Contents 1. [Schema Overview](#schema-overview) 2. [Database Setup](#database-setup) 3. [Schema Reference](#schema-reference) 4. [Related Documentation](#related-documentation) --- ## Schema Overview The database is organized into the following domains: ### Core Infrastructure (6 tables) | Table | Purpose | Primary Key | | ----------------------- | ----------------------------------------- | ----------------- | | `users` | Authentication credentials and login data | `user_id` (UUID) | | `profiles` | Public user data, preferences, points | `user_id` (UUID) | | `addresses` | Normalized address storage with geocoding | `address_id` | | `activity_log` | User activity audit trail | `activity_log_id` | | `password_reset_tokens` | Temporary tokens for password reset | `token_id` | | `schema_info` | Schema deployment metadata | `environment` | ### Stores and Locations (4 tables) | Table | Purpose | Primary Key | | ------------------------ | --------------------------------------- | ------------------- | | `stores` | Grocery store chains (Safeway, Kroger) | `store_id` | | `store_locations` | Physical store locations with addresses | `store_location_id` | | `favorite_stores` | User store favorites | `user_id, store_id` | | `store_receipt_patterns` | Receipt text patterns for store ID | `pattern_id` | ### Flyers and Items (7 tables) | Table | Purpose | Primary Key | | ----------------------- | -------------------------------------- | ------------------------ | | `flyers` | Uploaded flyer metadata and status | `flyer_id` | | `flyer_items` | Individual deals extracted from flyers | `flyer_item_id` | | `flyer_locations` | Flyer-to-location associations | `flyer_location_id` | | `categories` | Item categorization (Produce, Dairy) | `category_id` | | `master_grocery_items` | Canonical grocery item dictionary | `master_grocery_item_id` | | `master_item_aliases` | Alternative names for master items | `alias_id` | | `unmatched_flyer_items` | Items pending master item matching | `unmatched_item_id` | ### Products and Brands (2 tables) | Table | Purpose | Primary Key | | ---------- | ---------------------------------------------- | ------------ | | `brands` | Brand names (Coca-Cola, Kraft) | `brand_id` | | `products` | Specific products (master item + brand + size) | `product_id` | ### Price Tracking (3 tables) | Table | Purpose | Primary Key | | ----------------------- | ---------------------------------- | ------------------ | | `item_price_history` | Historical prices for master items | `price_history_id` | | `user_submitted_prices` | User-contributed price reports | `submission_id` | | `suggested_corrections` | Suggested edits to flyer items | `correction_id` | ### User Features (8 tables) | Table | Purpose | Primary Key | | -------------------- | ------------------------------------ | --------------------------- | | `user_watched_items` | Items user wants to track prices for | `user_watched_item_id` | | `user_alerts` | Price alert thresholds | `alert_id` | | `notifications` | User notifications | `notification_id` | | `user_item_aliases` | User-defined item name aliases | `alias_id` | | `user_follows` | User-to-user follow relationships | `follower_id, following_id` | | `user_reactions` | Reactions to content (likes, etc.) | `reaction_id` | | `budgets` | User-defined spending budgets | `budget_id` | | `search_queries` | Search history for analytics | `query_id` | ### Shopping Lists (4 tables) | Table | Purpose | Primary Key | | ----------------------- | ------------------------ | ------------------------- | | `shopping_lists` | User shopping lists | `shopping_list_id` | | `shopping_list_items` | Items on shopping lists | `shopping_list_item_id` | | `shared_shopping_lists` | Shopping list sharing | `shared_shopping_list_id` | | `shopping_trips` | Completed shopping trips | `trip_id` | | `shopping_trip_items` | Items purchased on trips | `trip_item_id` | ### Recipes (11 tables) | Table | Purpose | Primary Key | | --------------------------------- | -------------------------------- | ------------------------- | | `recipes` | User recipes with metadata | `recipe_id` | | `recipe_ingredients` | Recipe ingredient list | `recipe_ingredient_id` | | `recipe_ingredient_substitutions` | Ingredient alternatives | `substitution_id` | | `tags` | Recipe tags (vegan, quick, etc.) | `tag_id` | | `recipe_tags` | Recipe-to-tag associations | `recipe_id, tag_id` | | `appliances` | Kitchen appliances | `appliance_id` | | `recipe_appliances` | Appliances needed for recipes | `recipe_id, appliance_id` | | `recipe_ratings` | User ratings for recipes | `rating_id` | | `recipe_comments` | User comments on recipes | `comment_id` | | `favorite_recipes` | User recipe favorites | `user_id, recipe_id` | | `recipe_collections` | User recipe collections | `collection_id` | ### Meal Planning (3 tables) | Table | Purpose | Primary Key | | ------------------- | -------------------------- | ----------------- | | `menu_plans` | Weekly/monthly meal plans | `menu_plan_id` | | `shared_menu_plans` | Menu plan sharing | `share_id` | | `planned_meals` | Individual meals in a plan | `planned_meal_id` | ### Pantry and Inventory (4 tables) | Table | Purpose | Primary Key | | -------------------- | ------------------------------------ | ----------------- | | `pantry_items` | User pantry inventory | `pantry_item_id` | | `pantry_locations` | Storage locations (fridge, freezer) | `location_id` | | `expiry_date_ranges` | Reference shelf life data | `expiry_range_id` | | `expiry_alerts` | User expiry notification preferences | `expiry_alert_id` | | `expiry_alert_log` | Sent expiry notifications | `alert_log_id` | ### Receipts (4 tables) | Table | Purpose | Primary Key | | ------------------------ | ----------------------------- | ----------------- | | `receipts` | Scanned receipt metadata | `receipt_id` | | `receipt_items` | Items parsed from receipts | `receipt_item_id` | | `receipt_processing_log` | OCR/AI processing audit trail | `log_id` | ### UPC Scanning (2 tables) | Table | Purpose | Primary Key | | ---------------------- | ------------------------------- | ----------- | | `upc_scan_history` | User barcode scan history | `scan_id` | | `upc_external_lookups` | External UPC API response cache | `lookup_id` | ### Gamification (2 tables) | Table | Purpose | Primary Key | | ------------------- | ---------------------------- | ------------------------- | | `achievements` | Defined achievements | `achievement_id` | | `user_achievements` | Achievements earned by users | `user_id, achievement_id` | ### User Preferences (3 tables) | Table | Purpose | Primary Key | | --------------------------- | ---------------------------- | ------------------------- | | `dietary_restrictions` | Defined dietary restrictions | `restriction_id` | | `user_dietary_restrictions` | User dietary preferences | `user_id, restriction_id` | | `user_appliances` | Appliances user owns | `user_id, appliance_id` | ### Reference Data (1 table) | Table | Purpose | Primary Key | | ------------------ | ----------------------- | --------------- | | `unit_conversions` | Unit conversion factors | `conversion_id` | --- ## Database Setup ### Required Extensions | Extension | Purpose | | ----------- | ------------------------------------------- | | `postgis` | Geographic/spatial data for store locations | | `pg_trgm` | Trigram matching for fuzzy text search | | `uuid-ossp` | UUID generation for primary keys | --- ### Database Users This project uses **environment-specific database users** to isolate production and test environments: | User | Database | Purpose | | -------------------- | -------------------- | ---------- | | `flyer_crawler_prod` | `flyer-crawler-prod` | Production | | `flyer_crawler_test` | `flyer-crawler-test` | Testing | --- ## Production Database Setup ### Step 1: Install PostgreSQL ```bash sudo apt update sudo apt install postgresql postgresql-contrib ``` ### Step 2: Create Database and User Switch to the postgres system user: ```bash sudo -u postgres psql ``` Run the following SQL commands (replace `'a_very_strong_password'` with a secure password): ```sql -- Create the production role CREATE ROLE flyer_crawler_prod WITH LOGIN PASSWORD 'a_very_strong_password'; -- Create the production database CREATE DATABASE "flyer-crawler-prod" WITH OWNER = flyer_crawler_prod; -- Connect to the new database \c "flyer-crawler-prod" -- Grant schema privileges ALTER SCHEMA public OWNER TO flyer_crawler_prod; GRANT CREATE, USAGE ON SCHEMA public TO flyer_crawler_prod; -- Install required extensions (must be done as superuser) CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Exit \q ``` ### Step 3: Apply the Schema Navigate to your project directory and run: ```bash psql -U flyer_crawler_prod -d "flyer-crawler-prod" -f sql/master_schema_rollup.sql ``` This creates all tables, functions, triggers, and seeds essential data (categories, master items). ### Step 4: Seed the Admin Account Set the required environment variables and run the seed script: ```bash export DB_USER=flyer_crawler_prod export DB_PASSWORD=your_password export DB_NAME="flyer-crawler-prod" export DB_HOST=localhost npx tsx src/db/seed_admin_account.ts ``` --- ## Test Database Setup The test database is used by CI/CD pipelines and local test runs. ### Step 1: Create the Test Database ```bash sudo -u postgres psql ``` ```sql -- Create the test role CREATE ROLE flyer_crawler_test WITH LOGIN PASSWORD 'a_very_strong_password'; -- Create the test database CREATE DATABASE "flyer-crawler-test" WITH OWNER = flyer_crawler_test; -- Connect to the test database \c "flyer-crawler-test" -- Grant schema privileges (required for test runner to reset schema) ALTER SCHEMA public OWNER TO flyer_crawler_test; GRANT CREATE, USAGE ON SCHEMA public TO flyer_crawler_test; -- Install required extensions CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Exit \q ``` ### Step 2: Configure CI/CD Secrets Ensure these secrets are set in your Gitea repository settings: **Shared:** | Secret | Description | | --------- | ------------------------------------- | | `DB_HOST` | Database hostname (e.g., `localhost`) | | `DB_PORT` | Database port (e.g., `5432`) | **Production-specific:** | Secret | Description | | ------------------ | ----------------------------------------------- | | `DB_USER_PROD` | Production database user (`flyer_crawler_prod`) | | `DB_PASSWORD_PROD` | Production database password | | `DB_DATABASE_PROD` | Production database name (`flyer-crawler-prod`) | **Test-specific:** | Secret | Description | | ------------------ | ----------------------------------------- | | `DB_USER_TEST` | Test database user (`flyer_crawler_test`) | | `DB_PASSWORD_TEST` | Test database password | | `DB_DATABASE_TEST` | Test database name (`flyer-crawler-test`) | --- ## How the Test Pipeline Works The CI pipeline uses a permanent test database that gets reset on each test run: 1. **Setup**: The vitest global setup script connects to `flyer-crawler-test` 2. **Schema Reset**: Executes `sql/drop_tables.sql` (`DROP SCHEMA public CASCADE`) 3. **Schema Application**: Runs `sql/master_schema_rollup.sql` to build a fresh schema 4. **Test Execution**: Tests run against the clean database This approach is faster than creating/destroying databases and doesn't require sudo access. --- ## Connecting to Production Database ```bash psql -h localhost -U flyer_crawler_prod -d "flyer-crawler-prod" -W ``` --- ## Checking PostGIS Version ```sql SELECT version(); SELECT PostGIS_Full_Version(); ``` Example output: ```text PostgreSQL 14.19 (Ubuntu 14.19-0ubuntu0.22.04.1) POSTGIS="3.2.0 c3e3cc0" GEOS="3.10.2-CAPI-1.16.0" PROJ="8.2.1" ``` --- ## Schema Files | File | Purpose | | ------------------------------ | --------------------------------------------------------- | | `sql/master_schema_rollup.sql` | Complete schema with all tables, functions, and seed data | | `sql/drop_tables.sql` | Drops entire schema (used by test runner) | | `sql/schema.sql.txt` | Legacy schema file (reference only) | --- ## Backup and Restore ### Create a Backup ```bash pg_dump -U flyer_crawler_prod -d "flyer-crawler-prod" -F c -f backup.dump ``` ### Restore from Backup ```bash pg_restore -U flyer_crawler_prod -d "flyer-crawler-prod" -c backup.dump ``` --- ## Related Documentation - [Installation Guide](INSTALL.md) - Local development setup - [Deployment Guide](DEPLOYMENT.md) - Production deployment