🧩 App Patterns Explainer

The recurring design patterns used throughout J4H β€” and why each one matters

A design pattern is a reusable solution to a recurring problem. J4H is a small app but it uses a variety of well-known patterns to keep the code clean, testable, and maintainable. This explainer walks through each pattern, shows exactly where it appears in J4H, and explains why it was chosen.
πŸ—„οΈ
Repository Pattern
The Database class hides all storage details behind a single interface
β–Ό
Structural

A repository is an object that provides collection-like access to stored data. Callers ask for entries, summaries, or photos β€” they never write SQL directly. The repository decides how to store and retrieve the data.

  • Where: database.py β€” the Database class
  • add_entry(), get_entries(), delete_entry() are the repository methods for diary data
  • The class also provides identical interfaces for summaries, photos, settings, and family history
  • Routes in app.py call db.get_entries() β€” they never call SQL directly
  • Swapping SQLite for PostgreSQL required zero changes to the callers
app.py calling the repository
# Route code never sees SQL entries = db.get_entries(patient_id=patient_id, start_date=start) # The repository handles both database engines internally if self.use_postgres: cursor.execute('SELECT * FROM entries WHERE patient_id = %s', (id,)) else: cursor.execute('SELECT * FROM entries WHERE patient_id = ?', (id,))
Why it matters The test suite uses an in-memory SQLite database. Production runs PostgreSQL on Heroku. Because all data access goes through the repository, every test exercises the exact same code path as production β€” only the engine differs.
πŸ”Œ
Adapter Pattern
One Database class adapts two incompatible database engines
β–Ό
Structural

SQLite uses ? as a query placeholder. PostgreSQL uses %s. SQLite auto-increments with AUTOINCREMENT; PostgreSQL uses SERIAL. These two engines have incompatible interfaces. The adapter wraps them both so the rest of the app sees only one interface.

  • Where: database.py β€” the self.use_postgres flag throughout every method
  • The placeholder variable ('%s' or '?') is the adapter in action
  • Schema creation uses SERIAL vs AUTOINCREMENT based on the same flag
  • Results are normalized to dict using either RealDictCursor (psycopg2) or sqlite3.Row
  • All callers receive the same list[dict] regardless of which engine ran the query

SQLite (local dev)

Placeholder: ?
Auto-id: AUTOINCREMENT
Last insert: cursor.lastrowid

PostgreSQL (Heroku)

Placeholder: %s
Auto-id: SERIAL
Last insert: RETURNING id

βš™οΈ
Service Layer Pattern
Business logic lives in dedicated service classes, not in routes
β–Ό
Architectural

A service layer sits between the web layer (routes) and the data layer (database). It holds the business logic β€” the "how" of doing something, not just the "what." Routes become thin: they parse the request, call a service, and return the response.

  • Where: summarizer.py β€” HealthSummarizer class; fhir_service.py β€” FHIRService class
  • HealthSummarizer.generate_summary(entries, doctor_type, custom_doctor) handles all Claude API logic
  • The /api/summary route just fetches entries, calls the service, saves the result β€” no Claude API code in app.py
  • FHIRService handles all FHIR HTTP calls and data mapping β€” routes call fhir.get_patient_vitals()
  • Services can be tested independently without starting the Flask server
Thin route calling a service
@app.route('/api/summary', methods=['POST']) def generate_summary(): entries = db.get_entries(...) # data layer result = summarizer.generate_summary( # service layer entries, doctor_type, custom_doctor ) db.save_summary(result, ...) # data layer return jsonify({'summary': result}) # web layer
πŸŽ€
Decorator / Middleware Pattern
Cross-cutting concerns wrapped around route functions
β–Ό
Behavioral

A decorator wraps a function to add behavior before or after it runs β€” without modifying the function itself. Flask uses decorators for routing. J4H adds its own decorator for authentication.

  • Where: simple_auth.py β€” @passcode_required decorator; app.py β€” @app.route on every route
  • @passcode_required checks the session for a valid passcode before running the route function
  • If the passcode check fails it returns a 401 response β€” the wrapped function never runs
  • @app.route('/entries') is Flask's own decorator: it registers the function as the handler for that URL
  • Adding auth to a new route takes one line: @passcode_required above it
passcode_required wraps the route
def passcode_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get('authenticated'): return jsonify({'error': 'Unauthorized'}), 401 return f(*args, **kwargs) # run the real function return decorated @app.route('/api/entries') @passcode_required # stacked decorators def get_entries(): ...
πŸ”„
SPA-style Fetch Pattern
Flask renders the shell; JavaScript fetches and renders the data
β–Ό
Frontend

J4H is not a traditional server-rendered app where Flask builds the full HTML for each page. Instead, Flask returns a minimal HTML shell and JavaScript fetches the actual data from a JSON API, then renders it into the DOM. This is the Single-Page Application (SPA) pattern applied to a multi-page app.

  • Where: every page template β€” entries.html, calendar.html, chart.html, photos.html, etc.
  • Flask routes like /entries only call render_template('entries.html') β€” no database calls
  • On page load, JS calls fetch('/api/entries?patient_id=...') and renders the results
  • This is required for client-side decryption: the browser needs to decrypt entries before showing them
  • The server only sees encrypted ciphertext; the plaintext is never in the HTML
  1. Browser requests /entries β†’ Flask returns the HTML shell (no diary data)
  2. JS reads patient_id from localStorage and the AES key from sessionStorage
  3. JS calls fetch('/api/entries?patient_id=...') β†’ Flask returns JSON with encrypted entries
  4. JS decrypts each entry using the Web Crypto API
  5. JS renders the decrypted entries into the DOM
πŸ‘₯
Multi-tenant Pattern
One database, multiple patients β€” every query is scoped by patient_id
β–Ό
Architectural

J4H supports multiple patients sharing a single database. Every record in every table carries a patient_id column. Every API call filters by that ID. The currently selected patient is stored in localStorage on the client.

  • Where: all tables (entries, summaries, photos, family_history), all DB methods, all API routes
  • The client stores j4h_current_patient in localStorage as a JSON object with patient_id and patient_name
  • Every fetch call includes ?patient_id=... in the query string
  • The server passes the patient_id to db.get_entries(patient_id=...) β€” no record from another patient leaks through
  • Per-patient themes are stored in localStorage keyed by j4h_theme_<patientId>
Isolation without separate accounts There is no login per patient β€” just a shared passcode. The patient filter is a convenience separation, not a security boundary. The encryption key is shared across all patients on the same device.
πŸ”’
Encrypt-before-store Pattern
Sensitive data is encrypted on the client before it ever reaches the server
β–Ό
Security

Rather than trusting the server to protect data, this pattern encrypts data in the browser before sending it. The server stores only ciphertext. The decryption key never leaves the client.

  • Where: static/crypto.js β€” encryptText() and decryptText()
  • AES-GCM 256-bit via the browser's Web Crypto API
  • Key derived from the passcode via PBKDF2 (100,000 iterations, SHA-256)
  • Derived key stored in sessionStorage as base64 β€” wiped when the tab closes
  • Stored format: ENC:<iv_base64>:<ciphertext_base64>
  • Pain level is stored as a plaintext integer because it is needed for server-side chart aggregation

Encryption (on save)

Browser encrypts the text β†’ sends ENC:iv:ct to Flask β†’ Flask stores it as-is in PostgreSQL

Decryption (on load)

Flask returns ENC:iv:ct as JSON β†’ Browser decrypts it β†’ Plaintext rendered in DOM

πŸšͺ
Auth Gate Pattern
Every page and every API route checks authentication before doing anything
β–Ό
Security

Two separate auth gates enforce the passcode requirement: one on the server (Python decorator) and one on the client (JavaScript redirect). Both must be satisfied to access any protected resource.

  • Server gate: @passcode_required in simple_auth.py β€” protects API routes that return data
  • Client gate: static/auth-check.js β€” runs at the top of every page, redirects to /passcode if no valid session
  • The client gate prevents the page from even rendering for unauthenticated users (better UX)
  • The server gate prevents data from being served even if someone bypasses the client gate
  • Session is set server-side at POST /api/passcode after the passcode is verified
Defense in depth The client gate is a UX convenience β€” it can be bypassed by a determined user who disables JavaScript. The server gate is the real security boundary. Both layers are needed for a complete solution.
⏰
Background Scheduler Pattern
Timed jobs run independently of HTTP requests
β–Ό
Behavioral

Some work should happen on a schedule rather than in response to a user request. APScheduler runs background jobs inside the same Heroku dyno as Flask, executing independently of any incoming HTTP traffic.

  • Where: app.py β€” APScheduler initialized after Flask startup
  • daily_alert job: runs every 24 hours, emails the admin only if any service health check is warning or error
  • monthly_report job: runs on the 15th of each month at 9am UTC β€” always sends the full status report
  • Both jobs call the same internal health-check functions used by the /health dashboard
  • Jobs run concurrently using ThreadPoolExecutor β€” all 7 service checks run in parallel per execution
Scheduler setup in app.py
scheduler = BackgroundScheduler() scheduler.add_job(daily_alert, trigger='interval', hours=24) scheduler.add_job(monthly_report, trigger='cron', day=15, hour=9) scheduler.start()
πŸ“¦
Portable Data Pattern
Data can be exported as an encrypted file and reimported on any device
β–Ό
Data

Users should not be locked in to a single device or deployment. The portable data pattern gives them a self-contained encrypted snapshot of their data that can be restored anywhere the passcode is known.

  • Where: templates/export.html, templates/import.html, routes /export and /import
  • Export fetches all entries from the API, serializes them to JSON, and re-encrypts the whole blob as a .j4h file
  • The .j4h file is encrypted with the same AES-GCM key β€” it can only be opened on a device with the correct passcode
  • Import uploads the .j4h file, decrypts it client-side, and renders the entries without writing them back to the server
  • Export can be downloaded to disk or emailed directly from the export page
  1. User taps Export β†’ JS fetches encrypted entries from /api/entries
  2. JS decrypts all entries, re-serializes as JSON, then re-encrypts the whole JSON blob
  3. The encrypted blob is saved as j4h_export_YYYY-MM-DD.j4h
  4. On another device, user uploads the .j4h file β†’ JS decrypts and renders it β€” server never involved
⚑
Progressive Rendering Pattern
Show the page immediately; load data asynchronously
β–Ό
Frontend

Rather than blocking the browser until all data is ready, each page renders its skeleton immediately and shows a loading state while data is fetched asynchronously. This keeps the app feeling fast even on slow connections.

  • Where: every data page β€” entries, calendar, chart, photos, summaries, family history
  • The HTML shell loads in milliseconds; a "Loading…" spinner is shown while fetch() runs
  • When the fetch resolves, JS replaces the spinner with the rendered content
  • Photos in the gallery use loading="lazy" β€” only loaded when they scroll into view
  • The pain chart loads Chart.js only on the chart page, not on every page
Typical async load pattern
// On page load: show spinner immediately document.getElementById('list').innerHTML = '<div>Loading…</div>'; async function loadData() { const resp = await fetch('/api/entries?patient_id=' + id); const data = await resp.json(); renderList(data); // replace spinner with real content } loadData();
πŸ”§
Environment Configuration Pattern
Secrets and environment-specific settings live outside the codebase
β–Ό
Operational

API keys, database URLs, SMTP credentials, and other secrets must never be committed to version control. The twelve-factor app methodology stores all configuration in environment variables, injected at runtime by the platform.

  • Where: app.py uses os.getenv() for every secret; .env file (git-ignored) holds local values
  • DATABASE_URL β€” set by Heroku automatically when PostgreSQL is attached
  • ANTHROPIC_API_KEY β€” Claude AI key stored in Heroku config vars, never in the repo
  • MAIL_USERNAME / MAIL_PASSWORD β€” Gmail SMTP credentials
  • HEALTH_ALERT_EMAIL β€” admin alert destination; feature is disabled if not set
  • Locally, python-dotenv loads a .env file so the same code runs without any changes
Safe config access in app.py
# Never hardcode secrets β€” read from the environment api_key = os.getenv('ANTHROPIC_API_KEY') db_url = os.getenv('DATABASE_URL') mail_pw = os.getenv('MAIL_PASSWORD') # Provide safe defaults only for non-sensitive config port = int(os.getenv('PORT', 5000))

Patterns at a Glance

Repository Β· Adapter Β· Service Layer Β· Decorator Β· SPA Fetch Β· Multi-tenant Β· Encrypt-before-store Β· Auth Gate Β· Background Scheduler Β· Portable Data Β· Progressive Rendering Β· Environment Config