← back

selfhost

View on GitHub →

YadAI

A premium, self-hosted multi-user personal productivity and health dashboard built with Svelte 5 and SvelteKit. It unifies workout logging, yoga/meditation/cardio tracking via a one-tap activities check-in grid, local calendar sync with external iCal feeds, a personal expenses ledger with bank-statement PDF upload and Gemini LLM extraction, a shared family-gated notes system with folders, e-gift card tracking, birthday rewards, nutrition logging with native barcode scanning, a competitive leaderboard with customizable presets, a life activity feed, location maps, secure vaults, and a global search command bar — all integrated with a voice-enabled AI assistant with voice input and GDPR-compliant data portability.

Tech Stack

  • Languages: JavaScript, TypeScript, SQL
  • Frameworks & Libraries: SvelteKit 2, Svelte 5, Svelte Runes, Drizzle ORM, @auth/sveltekit, lucide-svelte, date-fns, node-ical
  • Tools & Databases: Supabase Database (PostgreSQL), postgres (pg driver), drizzle-kit, Vite, Vitest, Svelte-Check, Vercel, Google Calendar API, Gmail API, Open-Meteo API, Gemini AI API, BarcodeDetector API

Key Achievements (Resume Bullets)

  • Migrated the database access layer from legacy Supabase JS query builder chains to type-safe Drizzle ORM, establishing compile-time column-level verification while mapping 20+ low-access GDPR tables dynamically via raw runtime SQL templates to prevent schema file bloat.
  • Architected a Vitest-compatible mocked query builder proxy to intercept Drizzle calls (select, insert, update, delete, where, limit) and translate them to pre-existing Supabase mocked chains, preventing the rewrite of hundreds of test assertions while preserving test coverage.
  • Redesigned the workout logging schema and UI by moving set-level equipment weights into exercise defaults, implementing transient client inputs that sum plate weight and equipment weight before saving, resulting in a cleaner database schema and simplified progressive overload AI queries.
  • Refactored daily briefing modules from static mock values to a dynamic client/server data pipeline, implementing Google Calendar events, weather forecasts, and nutrition statistics using Svelte 5 runes ($derived and $effect), improving page load performance and scoping data security by user.
  • Unified Svelte 5 workout views by standardizing card styling, removing dead carousel and planner routes, and replacing absolute editing overlays with a centralized settings modal to resolve text overflows and visual noise.
  • Resolved a critical timezone-induced client hydration freeze on the /leaderboard route by configuring client-only rendering (ssr = false), bypassing DOM differences in UTC-mismatched Date calculations.
  • Fixed collapsible sidebar group interactions in Svelte 5 by untracking the expanded group state inside Svelte $effect triggers, preventing recursive reactive loops during manual toggle events.
  • Architected a single-user decoupled calendar sync engine parsing external iCal feeds (Luma, Partiful, Meetup) via node-ical, implementing strict SSRF filtering to block private/loopback IP subnets and securing feed URLs using AES-256-GCM encryption at rest.
  • Engineered an AI-driven expense ingestion pipeline that extracts transaction lists from bank statement PDFs using Gemini multimodal inline_data base64 uploads, staging them in a transient review database table with SHA-256 transaction-level fingerprinting to prevent duplicate imports.
  • Consolidated separate legacy workout, yoga, meditation, and running trackers into a unified occurrence-only habit check-in system, dropping redundant tables in favor of a single activity_log table with a unique composite index to ensure idempotent toggles.
  • Redesigned SvelteKit’s authentication hooks by implementing a path-segment regex API guard (/(^|\/)api\//) returning 401 Unauthorized for expired programmatic endpoints to prevent client-side JSON parsing crashes, while preserving redirection history for page views.
  • Mitigated decryption-induced client application crashes by changing the AES-256-GCM decryption utility to catch key rotation or corruption failures and return null, allowing downstream consumers to prompt Google OAuth re-authentication gracefully.
  • Rebranded the application to YadAI across 26 user-facing locations using a display-only strategy, safeguarding technical database encryption salts and session cookie keys from rename corruption.
  • Developed a database-backed caching system for nutrition logs, storing recent and favorite meals to bypass Gemini AI analysis latency, and integrated the native browser BarcodeDetector API for zero-dependency mobile camera barcode lookups.
  • Modularized a monolithic notes page by partitioning custom gift card balances and birthday rewards into dedicated /giftcards and /rewards routes, while rebuilding the /notes module with hierarchical folders and per-note family sharing.
  • Optimized the Svelte 5 workout logging interface for mobile viewports by introducing media-gated adaptive button labels, enforcing CSS flex-truncation properties on long exercise titles, and stacking weight/reps input elements vertically inside container-constrained views.

Core Architecture & Data Flow

The application follows SvelteKit routing conventions. Each route contains a +page.svelte UI module, accompanied by a +page.server.js load function for database and API access. Layouts (+layout.svelte) wrap pages to supply universal contexts (auth session, navigation state, command bar, mobile menus). Server loaders aggregate data from Supabase PostgreSQL via Drizzle ORM and multiple external APIs (Gmail, Google Calendar, Open-Meteo, Gemini AI, external iCal/ICS feeds). Client-side Svelte 5 runes manage reactive state, while API routes handle form actions, set saves, and session lifecycle mutations.

graph TD
    subgraph External APIs
        GMAIL[Gmail API]
        GCAL[Google Calendar API]
        WEATHER[Open-Meteo API]
        GEMINI[Gemini AI API]
        ICAL[External iCal Feeds]
        BARCODE[Native Barcode API]
    end

    subgraph SvelteKit Server
        AUTH["@auth/sveltekit OAuth"]
        LOADER["+page.server.js Loaders"]
        ACTIONS["Form Actions & API Routes"]
        DRIZZLE[Drizzle ORM]
        PRIVACY["privacy.js GDPR Pipeline"]
        LOGGER[Gated Structured Logger]
    end

    subgraph Database
        SUPA[Supabase PostgreSQL]
    end

    subgraph Client
        SVELTE["Svelte 5 Pages & Runes"]
        CMD["Command Bar Search"]
        CHAT["AI Chat + Voice Input"]
        PWA["PWA Share Target /insights"]
        CAMERA[Camera Barcode Scanner]
    end

    AUTH -->|OAuth tokens| GMAIL
    AUTH -->|OAuth tokens| GCAL
    LOADER --> DRIZZLE --> SUPA
    LOADER --> GMAIL
    LOADER --> GCAL
    LOADER --> WEATHER
    LOADER -->|node-ical SSRF Guard| ICAL
    ACTIONS --> DRIZZLE
    ACTIONS --> GEMINI
    PRIVACY -->|Dynamic SQL bulk export and delete| SUPA
    PWA -->|YouTube URL| GEMINI
    GEMINI -->|Insights| DRIZZLE
    SVELTE --> LOADER
    SVELTE --> ACTIONS
    CHAT --> GEMINI
    CAMERA -->|Barcode Lookup| BARCODE
    LOGGER -->|JSON Console Logs| ACTIONS

Architectural Trade-offs

DecisionSelected OptionConsidered AlternativesEngineering Rationale
Database ORM IntegrationDrizzle ORM with dynamic SQL for low-access tablesFull static mapping of all tables / Prisma ORMStatic mapping of 20+ tables accessed only for GDPR exports would bloat the schema file. Dynamic execution via db.execute(sql...) keeps schema code clean and maintainable.
Schema Drift PreventionORM adoption (Drizzle)Supabase CLI migrations (blocked by Docker) / Runtime validation / Schema snapshots / Remote type genORM provides compile-time column safety without Docker. Runtime checks and snapshots were rejected as hacky or manual-heavy.
Testing StrategyVitest Drizzle Proxy to Supabase mock converterRewriting all test files with new Drizzle mock objectsWriting a proxy interceptor wrapper bypassed rewriting hundreds of established test fixtures, preserving high coverage with minimal effort.
Workout Load RepresentationTotal weight in sets.weight + Exercise-level DefaultsPer-set equipment weight / Separate equipment tableRemoving set-level equipment weight prevents database redundancy. Storing defaults at the exercise level prefills form UI while keeping historical sets clean and simplifying progressive overload calculation.
Exercise Name UniquenessSimple DB-level normalized uniqueness (lowercase, trim, collapse whitespace)Heavy alias system / Parenthetical-stripping rules / Semantic merge layerComplex normalization hides behavior and creates long-term maintenance. Simple rules catch accidental duplicates while keeping stored data transparent and user-visible.
State ManagementSvelte Runes ($state, $derived, $effect)Svelte Stores / Redux / RxJSFine-grained, compile-time reactivity eliminates virtual-dom overhead and keeps bundle sizes minimal.
SSR ConfigurationClient-Only Rendering (ssr = false) on Time-Sensitive PagesPure Server-Side Rendering (SSR) / Server-synced timestampsTimezone differences on server vs client date calculations produce different seeds, crashing Svelte 5’s DOM reconciliation. Client-only rendering guarantees a clean mount.
Route StructureModular subroutes (/workout, /yoga, /meditation)Monolithic /wellness routeSpecific subroute layouts isolate loader computations, reduce component complexity, and enable targeted command bar search intent routing.
Mock Data StrategyStrict data-driven architecture with empty statesHybrid mock-data fallbacksMock fallbacks masked API failures, violated data-scoping rules, and bypassed timezone-sensitive calculations. Empty states prompt users to configure real integrations.
Gmail IntegrationLive SvelteKit server loaders with Gmail APIClient-side parsing and static simulationDelivers fully dynamic inbox (read, send, draft, archive, star, triage) for authenticated users instead of fake mock data.
OAuth Scope RecoverySelf-healing reconnect CTA + removal of loader AI enrichmentManual reauth redirect with synchronous LLM enrichment in loaderInline reconnect CTA is more discoverable. Stripping synchronous Gemini calls from server loaders eliminates blocked page transitions and duplicate API calls from SvelteKit link prefetching.
Media Capture StrategyPWA Web Share Target → Gemini AI extractionYouTube Data API watch history (blocked) / Spotify API (free-tier restricted)YouTube blocks watch history API. PWA share target uses native mobile share sheets for zero-effort video logging without complex API configurations.
Chat Input UXState-toggleable voice/keyboard layoutsPermanent side-by-side text + mic elementsToggle preserves minimalist aesthetics, prevents accidental submissions, and optimizes spacing for mobile viewports.
Gym Workout UXSingle-page grid with bottom-sheet modals + safe-area notch spacingSequential carousel navigation with floating hamburger iconBottom-sheet modals allow logging exercises in any order. Safe-area CSS variables prevent camera notch clipping. Eliminates broken drag-and-drop and floating button overlap bugs.
List Component DesignParent-fetches data + pre-grouped [{label, items}] arrayInternal URL-based auto-fetch / Auto-grouping via groupKey propParent data ownership supports inline editing and mutations. Pre-grouped arrays give full control over sort order and label formatting without internal grouping complexity.
Command Bar StylingVanilla CSS with ⌘K trigger and micro-animationsUncompiled Tailwind CSS utilitiesVanilla CSS guarantees correct compilation without Tailwind dependency. Custom dark/light mode tokens align with the application’s design system.
Calendar Sync ScopeSingle-User Decoupled Core with local display preferencesFull Port with bidirectional social sharing graphsSocial features add unnecessary database and UI complexity for a single household. Local localStorage preferences eliminate database writes.
Rebranding StrategyDisplay-Text-Only rename (YadAI)Global search-and-replaceGlobal replacing would overwrite the KDF_SALT in encryption.js and cookie names, permanently corrupting existing encrypted database data and user sessions.
Decryption ResiliencyReturn null on AES decryption failuresLet errors bubble up / Fallback to raw ciphertextBubble-up requires wrapping every loader in try-catch. Ciphertext propagation crashes downstream APIs. null gracefully acts as expired sessions, triggering standard re-auth flows.
Expense Capture IntakePDF Statement LLM Extraction (Gemini)Plaid / Teller API / CSV Manual MappingPlaid’s free trial is capped at 10 bank links; Teller’s reverse-engineered API is fragile. PDF LLM extraction has $0 recurring cost, zero credentials stored, and uses existing Gemini infrastructure.
Logging ArchitectureGated Structured Logger with severity thresholdsLocal console overrides / DB log tableDB log table causes high write overhead; local overrides lack standardization. Severity gating (LOG_LEVEL) silences hover/prefetch console clutter while keeping JSON metadata queryable.
Activities Domain StructureUnified one-tap check-in grid page (/activities)Independent pages for /yoga, /meditation, and /runLegacy trackers were write-only and timer-focused. Eliminating timers and self-reported estimations in favor of occurrence-only habit grids reduces codebase size and database bloat.
Notes ArchitectureFree-form markdown notes with folders & folders/note sharingDynamic Airtable-style columns & tablesDynamic columns require runtime DDL (security risk) or JSONB query wrappers. Free-form notes offer maximum user flexibility with a single, simple schema.
Nutrition Logging SpeedRecents/Favorites cache + native browser BarcodeDetectorMandatory AI extraction on every meal / Cloud barcode lookupCaching favorites cuts latency from seconds to immediate. Native browser barcode APIs keep dependencies at zero and require no paid third-party catalog keys.
Leaderboard RulesSchema-Defined Presets (workouts, protein, consistency)Dynamic DB-driven custom rulesDynamic rule builders require elaborate schemas and validators. Code-defined presets are trivial to maintain and cover 100% of user needs.
Workout Set Input UXVertically stacked inputs on separate cardsFlex-wrapped horizontal single-card layoutStacking elements vertically guarantees 100% viewport width on mobile, avoiding input squishing and allowing large touch targets for numeric buttons.

Technical Challenges & Deep Dives

1. Drizzle ORM Custom Vitest Promise-Chaining Proxy

  • Problem: During the migration to Drizzle ORM, the existing test suite failed because the tests mocked Supabase client method chains (supabase.from().select()), whereas production code began using Drizzle query builders (db.select().from().where()). Completely rewriting all mock structures across the test codebase was a huge, error-prone task. Additionally, custom database calls chained thenables (e.g. .then(res => res[0])), requiring accurate Promise propagation.
  • Solution: Programmed a Vitest custom proxy interceptor under $lib/server/db that captures Drizzle-style builder invocations (e.g. select, update, insert, delete, and chained clauses like .where() or .limit()) and translates them dynamically to the existing mocked Supabase chains. To support thenable unpacking in production loaders, the proxy implements custom .then() wrappers resolving to standard JavaScript promises: promise.then(onFulfilled, onRejected).
  • Key Takeaway: An API proxy translation layer can successfully bridge disparate database interfaces in testing contexts, preventing wide-scale test rewrite churn during database migrations.

2. Workout Database Denormalization vs. Transient Summation

  • Problem: The workout tracker originally stored equipment_weight alongside weight on every set, which led to repeated machine data, ambiguous weight meaning (plate load vs total load), and complex AI routines when calculating targets for progressive overload.
  • Solution: Standardized workout_sets.weight to hold the total logged weight. Added default weight and unit fields on workout_exercises. The UI now fetches the default equipment weight to prefill the client input form, handles variations locally in the client state, and submits them to the backend where they are combined into a single total weight for database storage. If an incoming transient weight is greater than zero, the backend automatically updates the exercise-level defaults.
  • Key Takeaway: Keeping historical set records clean of configuration properties simplifies queries, reduces database size, and ensures recommendations process a single source of truth.

3. Timezone-Induced Hydration Reconciliation Failures

  • Problem: The Leaderboard page calculates weekly statistics and scores using seed-based calculations derived from the current week number. When a user loaded the dashboard near a midnight or week boundary, a server in UTC and a browser in a different timezone (e.g., UTC-4) calculated different week numbers. This generated different scoring arrays, sorting the lists in a mismatched order during client-side hydration, which caused Svelte 5’s keyed loop reconciler to crash and freeze the loading screen.
  • Solution: Created a dedicated +page.js route configuration setting export const ssr = false;. This forces SvelteKit to skip server-side rendering for this route and generate the page solely on the client. Timezone calculations are therefore locked to the client environment, eliminating reconciliation conflicts.
  • Key Takeaway: Any page utilizing timezone-sensitive calculations or local current timestamps must be loaded either dynamically via serialized server load states or rendered purely client-side to prevent hydration freezes.

4. Svelte 5 Reactive Loop Collapses in Collapsible Sidebars

  • Problem: The navigation sidebar utilizes an effect to automatically expand the parent folder of the currently active route on page load. However, because the expansion statement mutated and read the global folder state using an object spread (openGroups = { ...openGroups, [activeGroup]: true }), Svelte tracked openGroups as a dependency of the effect. Manual collapse clicks updated openGroups, triggering the effect again and forcing the active folder back to open, rendering the toggle buttons non-functional.
  • Solution: Wrapped the object update statement inside Svelte 5’s untrack() function. This prevents Svelte from registering openGroups as a reactive dependency inside the effect, restricting execution to when the activeGroup route actually changes.
  • Key Takeaway: Always use untrack when mutating reactive state inside $effect triggers that read or spread the mutated variable, preventing recursive loops.

5. Gmail OAuth Scope Drift and Loader-Blocking AI Enrichment

  • Problem: Users who authenticated before the gmail.modify scope was added received 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT errors when accessing the mail client. Simultaneously, a synchronous Gemini AI call embedded inside the server loader to generate email summaries and reply suggestions blocked page transitions on every navigation, and SvelteKit’s link prefetching triggered duplicate Gemini API requests on hover.
  • Solution: Implemented a self-healing OAuth reconnect flow: the Gmail API client catches scope/permission errors, propagates them to the loader, and the mail page renders an inline “Reconnect Account” CTA pointing to /signin?prompt=consent. Stripped the synchronous Gemini summary block from the server loader entirely, eliminating blocked navigations and redundant prefetch-triggered API calls.
  • Key Takeaway: Server loaders in SvelteKit should never contain blocking third-party API calls because link prefetching will trigger them invisibly on hover. Scope evolution in OAuth integrations requires self-healing reconnect flows, not manual user instructions.

6. ResizeObserver Subpixel Feedback Loop in Family Tree

  • Problem: The Family Tree component used a ResizeObserver to measure its container and dynamically set child wrapper dimensions in pixels. When the browser rounded subpixel measurements differently between reads and writes, the observer detected a size change on every frame, creating an infinite feedback loop that froze the component on mount.
  • Solution: Replaced the container-level ResizeObserver with a standard window resize event listener. Window resize events only fire on actual viewport changes, breaking the subpixel measurement-mutation cycle entirely.
  • Key Takeaway: Avoid using ResizeObserver on elements whose children are sized in response to the observed measurement. Use window-level resize listeners or debounced observers when the layout is sensitive to fractional pixel rounding.

7. PWA Share Target as YouTube Watch History Bypass

  • Problem: Two intended media tracking APIs were unavailable: YouTube’s Data API blocks access to private user watch history playlists (HL), and Spotify’s Web API restricts free-tier accounts from querying playback data. Without either API, the /life activity feed had no way to capture media consumption from mobile devices.
  • Solution: Registered the application as a Progressive Web App (PWA) share target pointing to /insights. When users share a YouTube URL from their phone’s native share sheet, the endpoint parses the link, calls the Gemini AI API to extract video insights and key takeaways, and persists the record in the media_insights database table. The /life page loader then queries this table alongside other activity sources to build a unified feed.
  • Key Takeaway: When official APIs are blocked or restricted, PWA Web Share Target registration provides a zero-friction mobile data ingest path that leverages the operating system’s native share sheet without requiring complex API credentials or elevated account tiers.

8. Safe-Area Notch Collision and Floating Element Overlap on Mobile

  • Problem: The gym tracking page rendered a floating fixed-position hamburger menu icon that overlapped with page titles and action buttons on mobile. On phones with camera notches, the top content of the application layout was clipped or obscured by the notch and status bar area.
  • Solution: Replaced the floating hamburger icon with a sticky header bar that houses the navigation toggle. Applied env(safe-area-inset-top) CSS environment variables to the shell and page-level containers, dynamically adjusting top spacing based on the device’s notch geometry. Bottom-sheet modals replaced top-of-page navigation to eliminate header collision entirely.
  • Key Takeaway: Mobile-first layouts must account for hardware-specific safe areas using CSS env() variables. Floating fixed-position elements should be avoided in favor of sticky containers that participate in the document flow.

9. Complete Supabase-to-Drizzle Database Migration

  • Problem: The server codebase originally had an architectural split where some domain modules accessed PostgreSQL via the Supabase client (supabase.from()), while others used Drizzle ORM (db). This created inconsistencies, multiple DB libraries in the runtime, and required maintaining two query layers. Additionally, several tables (family tree, gift cards, insights, measurements, notes, runs, job applications) were not defined in Drizzle’s schema.ts.
  • Solution: Conducted a comprehensive database layer convergence. Added Drizzle schema definitions for all remaining tables to schema.ts after introspecting the columns. Rewrote all remaining *-db helpers, authentication helpers (identity.js), and lifecycle handlers (onboarding.js, job-suggestions.js) to use Drizzle’s type-safe query builders. Re-implemented the AI engine’s dynamic Postgres query tool using Drizzle table mappings. Purged the legacy supabase.js client once all consumers were migrated. To keep unit tests green without full rewrites, integrated Drizzle’s PgDialect query compiler inside Vitest mocks to parse Drizzle queries back to the original chainable mock expectations.
  • Key Takeaway: Major database layer migrations can be performed incrementally by mapping schema definitions first, refactoring query boundaries, and using automated SQL/dialect compilers to translate mock objects in the testing suite.

10. Server-Side Request Forgery (SSRF) Protection in iCal Feed Synchronizer

  • Problem: Implementing calendar syncing allows users to input arbitrary URLs for external iCal/ICS feeds (e.g. Luma, Partiful). A user or malicious party could supply loopback or link-local addresses (e.g. 127.0.0.1, 169.254.169.254), tricking the server into making requests to internal services (SSRF) or accessing cloud provider metadata endpoints.
  • Solution: Programmed an IP-resolution and network boundary guard inside the feed downloader. Before executing the HTTP request, the URL domain is resolved via DNS to its IP addresses. Every resolved IP is verified against private, loopback, link-local, and broadcast subnet ranges. If any address falls inside these forbidden blocks, the sync run is aborted immediately.
  • Key Takeaway: User-supplied URLs for server-side fetching must be explicitly resolved and validated against network boundaries to protect local infrastructure from unauthorized access.

11. Idempotent Deduping of Multimodal Document Ingestion

  • Problem: Ingesting bank statements via Gemini AI PDF extraction returns a list of transactions that are reviewed by the user. If the user uploads overlapping statements, identical transactions would be written multiple times, corrupting monthly spending calculations.
  • Solution: Designed a deterministic SHA-256 fingerprint helper that hashes properties of each transaction: sha256(userId|accountLabel|postedDate|amount|direction|normalizedDescription). Added a unique index on (userId, fingerprint) in the database. Transactions are staged in a transient expense_imports table for review. Upon confirmation, a bulk insertion executes ON CONFLICT DO NOTHING, guaranteeing that duplicate transactions across overlapping statements are silently ignored without throwing database exceptions.
  • Key Takeaway: Generating a deterministic hash of transactional attributes enables idempotent database syncs without relying on external system IDs.

12. API Routing Authentication vs SvelteKit Client Navigation Data Calls

  • Problem: Bouncing unauthenticated users to /signin inside SvelteKit server hooks worked for page requests, but programmatic API calls (e.g., /api/chores) would also return a 200 OK HTML sign-in page. This caused JSON parsing errors on the client. However, simple path-prefix checks crashed standard SvelteKit page-transitions, which fetch JSON data via internal URL extensions ending in __data.json with an Accept: application/json header.
  • Solution: Implemented a regex path-segment guard /(^|\/)api\// in the server hook. It checks if the URL contains the segment /api/ (either at the start or nested), which isolates user-defined programmatic REST routes. If a request is caught by the regex and unauthenticated, it returns a clean 401 Unauthorized response with JSON, allowing client components to handle expiration, while standard page-transition endpoints bypass the check.
  • Key Takeaway: Distinguishing programmatic APIs from framework-specific page-hydration endpoints requires matching explicit route segments rather than checking general HTTP accept headers.

System Performance & Key Metrics

  • Test Coverage: 162+ Vitest unit tests passing across workout logic, AI exercise targets, jobs, language, and detailed database history modules.
  • Compiler Safety: svelte-check with --fail-on-warnings produces 0 errors and 0 warnings across the codebase.
  • Build Integrity: Production bundle builds successfully via Vite with zero compilation errors.
  • Application Scale: 39+ registered routes, 60 architectural decision records (ADRs), 5+ external API integrations (Gmail, Google Calendar, Open-Meteo, Gemini AI, external iCal/ICS feeds).
  • Uptime/Stability: SSR hydration freeze on the leaderboard route resolved via client-only rendering. ResizeObserver infinite loop on the family tree component eliminated. Zero remaining navigation or rendering exceptions in production.