Skip to content

Hkayy47/TextBuddy

Repository files navigation

TextBuddy 🤙

Your personal AI text companion. Talk about your day, set reminders, get check-ins — like texting a friend who's always there.


Table of Contents

  1. What Is TextBuddy?
  2. Tech Stack
  3. Repository Structure
  4. Getting Started
  5. Backend Architecture
  6. Frontend Architecture
  7. AI Integration (Gemini)
  8. Security Architecture
  9. Notification Strategy
  10. API Reference
  11. Environment Variables
  12. Deployment
  13. CI/CD

What Is TextBuddy?

TextBuddy is a mobile-first AI companion app. Users text it like they'd text a close friend — casually, emotionally, randomly. It listens, responds with warmth and intelligence (powered by Google Gemini), learns the user's patterns within a conversation, and sends smart push notifications (reminders, check-ins, follow-ups) based on what the user has told it.

Core Loop

  1. User sends a message (text)
  2. Gemini processes it with full conversation history + user profile
  3. Gemini replies empathetically and helpfully
  4. If a reminder/task is detected, it's scheduled via the notification engine
  5. Notifications fire at the right moment — not spam, but felt

Emotional Design Principle

TextBuddy should feel like the most attentive, non-judgmental friend you've ever had. Never clinical. Never robotic. Always warm.


Tech Stack

Backend

Technology Role
Node.js 20+ / TypeScript Runtime & language
Fastify HTTP framework (faster than Express, excellent TS support)
PostgreSQL 16 (Neon in production) Primary database — stores users, conversations, messages, reminders
Prisma ORM — type-safe database queries, schema migrations
Google Gemini (gemini-2.5-flash) AI engine via @google/genai SDK (free tier)
Firebase Cloud Messaging Push notifications to mobile/web
bcryptjs Password hashing (cost factor 12)
Zod Runtime input validation on every API route
Pino PII-safe structured logging

Frontend

Technology Role
React 19 UI framework
Vite 5 Build tool & dev server
Vanilla CSS Full design system — no Tailwind, maximum control
Framer Motion Animations (message bubbles, typing indicator, page transitions)
Zustand Lightweight state management (auth store)
TanStack Query v5 Server state + data fetching
vite-plugin-pwa (v0.20.x) Progressive Web App with service workers
Lora + DM Sans Typography (Google Fonts) — Lora for display warmth, DM Sans for readable body text

Infrastructure

Technology Role
Docker Compose Local development (Postgres container)
GitHub Actions CI/CD pipeline
Vercel Frontend and backend hosting (production) — backend runs as Vercel serverless functions
Neon Managed Postgres (Vercel Marketplace, free tier) — serverless-pooled connections
External scheduler (e.g. cron-job.org) Polls the reminder-firing endpoint every minute (free — Vercel Hobby cron only runs ~daily)

Repository Structure

textbuddy/
├── apps/
│   ├── api/                        # Fastify backend (deployed as Vercel serverless functions)
│   │   ├── api/
│   │   │   └── index.ts            # Vercel function entry — wraps the Fastify app
│   │   ├── src/
│   │   │   ├── config/
│   │   │   │   ├── env.ts          # Zod-validated env vars
│   │   │   │   ├── db.ts           # Prisma client singleton
│   │   │   │   └── gemini.ts       # Google GenAI client
│   │   │   ├── modules/
│   │   │   │   ├── ai/             # Gemini AI prompt engine
│   │   │   │   ├── auth/           # Registration, login, token refresh
│   │   │   │   ├── conversations/  # Conversation list & delete
│   │   │   │   ├── cron/           # Reminder-polling endpoint (secret-authed)
│   │   │   │   ├── messages/       # Chat send/receive
│   │   │   │   ├── notifications/  # FCM push + reminder persistence
│   │   │   │   ├── reminders/      # Reminder CRUD
│   │   │   │   └── users/          # Profile, preferences, FCM token, account deletion
│   │   │   ├── utils/
│   │   │   │   ├── encryption.ts   # AES-256-GCM message encryption
│   │   │   │   ├── sanitize.ts     # Input sanitization + PII masking
│   │   │   │   └── logger.ts       # Pino logger (no PII in logs)
│   │   │   ├── app.ts              # Configured Fastify app (shared by server.ts and api/index.ts)
│   │   │   └── server.ts           # Local-dev entry point (app.listen)
│   │   ├── prisma/
│   │   │   └── schema.prisma       # Database schema
│   │   ├── .env.example
│   │   ├── Dockerfile
│   │   ├── vercel.json             # Routes all requests to api/index.ts
│   │   └── package.json
│   └── web/                        # React PWA
│       ├── src/
│       │   ├── components/
│       │   │   ├── Chat/           # Chat, TextInput, TypingIndicator
│       │   │   ├── Message/        # MessageBubble
│       │   │   └── Onboarding/     # Login, Welcome
│       │   ├── stores/             # Zustand auth store
│       │   ├── hooks/              # useMessages custom hook
│       │   ├── lib/                # API client, notification setup
│       │   ├── App.tsx             # Root component (routing)
│       │   ├── main.tsx            # React entry point
│       │   └── index.css           # Complete design system
│       ├── public/
│       │   ├── sw.js               # Service worker for push
│       │   └── manifest.json       # PWA manifest
│       ├── index.html
│       ├── vite.config.ts
│       └── package.json
├── packages/
│   └── shared/                     # Shared TypeScript types
│       └── types.ts
├── docker-compose.yml              # Local dev (Postgres)
├── .github/workflows/ci.yml        # CI pipeline
├── .gitignore
└── package.json                    # Root workspace config

What Each Directory Does

Directory Purpose
apps/api/ The entire backend server — handles HTTP requests, AI processing, database operations, and push notifications
apps/api/api/ Vercel serverless function entry — wraps the Fastify app for deployment
apps/api/src/config/ Configuration files — environment validation, database/AI client singletons
apps/api/src/modules/ Feature modules — each module has its own route file (and service file where needed)
apps/api/src/utils/ Shared utilities — encryption, sanitization, logging
apps/api/prisma/ Database schema and migrations
apps/web/ The entire frontend React application
apps/web/src/components/ React UI components organized by feature
apps/web/src/stores/ Zustand state management stores
apps/web/src/hooks/ Custom React hooks for business logic
apps/web/src/lib/ API client, Firebase notification setup
apps/web/public/ Static assets — service worker, PWA manifest
packages/shared/ TypeScript types shared between backend and frontend

Getting Started

Prerequisites

  1. Node.js 20+Download
  2. DockerDownload (for local Postgres)
  3. Google Gemini API Key (free) — Get one at Google AI Studio
  4. Firebase Project (optional for push notifications) — see Firebase Setup below

Step 1: Clone & Install

git clone https://github.com/YOUR_USERNAME/textbuddy.git
cd textbuddy
npm install

Step 2: Start Database

docker-compose up -d

This starts PostgreSQL on localhost:5432 (user: textbuddy, password: password, db: textbuddy).

Step 3: Configure Backend

cd apps/api
cp .env.example .env

Edit apps/api/.env and fill in:

Variable What to put
GEMINI_API_KEY Your API key from Google AI Studio
JWT_SECRET Run: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
JWT_REFRESH_SECRET Run the same command again (use a different value)
MESSAGE_ENCRYPTION_KEY Run the same command again (use a different value)
CRON_SECRET Run the same command again (use a different value) — protects the reminder-polling endpoint

DATABASE_URL/DIRECT_URL are pre-configured for the Docker container (both point at the same local Postgres; in production they differ — see Deployment).

Step 4: Run Database Migrations

cd apps/api
npx prisma migrate dev --name init

This creates all the database tables (users, conversations, messages, reminders, sessions).

Step 5: Configure Frontend

cd apps/web
cp .env.example .env

The default VITE_API_URL=http://localhost:3001 is already correct for local dev.

Step 6: Start Development Servers

In two separate terminals:

# Terminal 1 — Backend (port 3001)
cd apps/api
npm run dev

# Terminal 2 — Frontend (port 5173)
cd apps/web
npm run dev

Step 7: Open the App

Go to http://localhost:5173 in your browser.

  1. You'll see the Welcome screen — click "Get Started"
  2. Register a new account (email + password, minimum 10 characters)
  3. Start chatting with TextBuddy! 🎉

Firebase Setup (Optional)

Push notifications require a Firebase project. You can skip this for initial testing — the app works without push notifications.

To set up push notifications:

  1. Go to Firebase Console
  2. Create a new project (or use an existing one)
  3. Enable Cloud Messaging in the project settings
  4. Generate a new Web push certificate (this gives you the VAPID key)
  5. Create a Service Account and download the JSON credentials
  6. Add the Firebase config to both .env files:
    • Backend (apps/api/.env): FIREBASE_PROJECT_ID, FIREBASE_PRIVATE_KEY, FIREBASE_CLIENT_EMAIL
    • Frontend (apps/web/.env): VITE_FIREBASE_API_KEY, VITE_FIREBASE_AUTH_DOMAIN, etc.

Backend Architecture

Modules Explained

Auth Module (modules/auth/)

Handles user registration, login, and JWT token refresh.

  • Registration: Validates email + password (min 10 chars), hashes password with bcrypt (cost 12), creates user, returns JWT access + refresh tokens
  • Login: Validates credentials, returns fresh tokens
  • Token Refresh: Accepts a refresh token, returns a new short-lived access token (15 min). This enables seamless re-authentication without re-entering credentials.

AI Module (modules/ai/)

The heart of TextBuddy — the Gemini integration and personality engine.

  • Contains the complete system prompt that defines TextBuddy's personality: warm, empathetic, casual, honest, occasionally funny
  • Handles reminder detection: When a user says "remind me to...", the AI extracts the reminder details and includes them as structured data in its response
  • Handles check-in detection: When a user mentions a future event, the AI may offer to check in afterward
  • Transforms conversation history from app format (assistant role) to Gemini format (model role, with parts array)

Messages Module (modules/messages/)

The main chat endpoint.

  • Send message: Receives user text → saves encrypted message → fetches last 40 messages for context → calls Gemini → saves encrypted AI response → optionally schedules reminders → returns AI reply
  • Load history: Retrieves and decrypts all messages in a conversation

Conversations Module (modules/conversations/)

Manages the conversation list.

  • List conversations: Returns all user conversations sorted by most recent, with a preview of the last message (decrypted, truncated to 80 chars)
  • Delete conversation: Removes a conversation and all its messages (cascade delete)

Notifications Module (modules/notifications/)

Handles push notification delivery via Firebase Cloud Messaging (FCM).

  • Schedule reminder: Creates a database record with a scheduledAt timestamp — firing is handled later by the cron-polling endpoint (see Cron Module below)
  • Send push notification: Checks user preferences (enabled? quiet hours?) before sending via FCM
  • Quiet hours: Notifications are suppressed during the user's configured quiet hours (default 10pm–8am)

Cron Module (modules/cron/)

A secret-authenticated polling endpoint that fires due reminders — replaces the old in-process BullMQ worker, which needed a long-lived process this serverless deployment can't host.

  • POST /internal/cron/process-reminders: Requires Authorization: Bearer <CRON_SECRET>. Finds all reminders where scheduledAt <= now and fired = false, sends the push notification for each, and marks them fired. An external scheduler (e.g. cron-job.org, free) calls this every minute — see Deployment.

Reminders Module (modules/reminders/)

CRUD operations for reminders.

  • List reminders: Returns all reminders (fired and upcoming) for the current user
  • Cancel reminder: Deletes a reminder row (it simply won't be picked up by the next poll)

Users Module (modules/users/)

User profile and account management.

  • Get profile: Returns current user's profile
  • Update preferences: Modify display name, timezone, notification settings, quiet hours
  • Register FCM token: Saves the device's push notification token
  • Delete account: Permanently deletes the user and ALL associated data (cascade: conversations, messages, reminders, sessions)

Utilities

Encryption (utils/encryption.ts)

All message content is encrypted at rest using AES-256-GCM:

  • Each message gets a unique IV (initialization vector) — never reused
  • An auth tag is stored for tamper detection
  • The encryption key is a 32-byte value stored only in environment variables
  • Plaintext message content is never stored in the database

Sanitization (utils/sanitize.ts)

  • Strips HTML tags from user input
  • Collapses whitespace while preserving newlines
  • Masks email addresses for safe logging (e.g., joh***@example.com)

Logger (utils/logger.ts)

  • Uses Pino (same logger Fastify uses internally)
  • Never logs: message content, full email addresses, JWT tokens, request bodies, or headers
  • Logs: request method/URL, response codes, error types, and timing
  • Pretty-prints in development, structured JSON in production

Frontend Architecture

App Flow

  1. Welcome Screen → First-time visitors see an animated onboarding page explaining what TextBuddy does
  2. Login/Register → Email + password authentication (auto-detects user's timezone on registration)
  3. Chat Interface → The main experience — full-screen mobile-first chat with TextBuddy

Components

Chat.tsx — Main Chat Container

  • Renders the header (avatar, name, status, action buttons)
  • Displays the message list with auto-scroll to bottom
  • Shows an empty state with conversation starter suggestions
  • Shows a typing indicator while waiting for AI response
  • Includes error display for failed messages

TextInput.tsx — Message Input

  • Auto-growing textarea (up to 120px height)
  • Enter sends the message, Shift+Enter adds a newline
  • Send button with SVG icon — disabled when empty or loading
  • Respects mobile safe areas (notch, home indicator)

TypingIndicator.tsx — AI Thinking Animation

  • Three bouncing dots with staggered animation
  • Spring-based entrance/exit animations via Framer Motion

MessageBubble.tsx — Individual Messages

  • User messages: Dark background, right-aligned, rounded with small bottom-left corner
  • AI messages: Light background with border, left-aligned, rounded with small bottom-right corner
  • Timestamp below each bubble
  • Spring animation on appearance (scale + translate)

Login.tsx — Authentication Form

  • Toggles between Login and Register modes
  • Validates email and password (10+ chars for registration)
  • Auto-detects timezone on registration
  • Error display for invalid credentials or server issues

Welcome.tsx — Onboarding Screen

  • Animated logo entrance (spring + rotation)
  • Staggered feature cards with icons
  • Gradient background with floating ambient orbs
  • "Get Started" button transitions to auth

State Management

Auth Store (stores/auth.store.ts)

Zustand store with persistence:

  • Stores JWT tokens, user ID, and welcome-seen flag
  • Persists refresh token and user ID to localStorage for session continuity
  • logout() clears all state

Custom Hooks

useMessages() — Chat Logic

  • Manages local message state
  • Optimistic updates: User message appears immediately before server response
  • Handles send, load conversation, start new conversation
  • Error recovery: removes optimistic message on failure

API Client (lib/api.ts)

  • Wraps fetch with automatic auth header injection
  • Automatic token refresh: If a request returns 401, it tries to refresh the token and retries
  • Falls back to logout if refresh fails

Push Notifications (lib/notifications.ts)

  • Dynamically imports Firebase SDK (avoids errors when unconfigured)
  • Requests notification permission from the browser
  • Registers the FCM token with the backend
  • Handles foreground messages (shows browser notification)

AI Integration (Gemini)

Why Gemini?

This project uses Google Gemini 2.5 Flash instead of Anthropic Claude:

  • Free tier available — get an API key at Google AI Studio
  • Fast responses — Flash models are optimized for speed
  • Capable enough for conversational, empathetic responses

How It Works

The AI engine (modules/ai/ai.service.ts) uses the @google/genai SDK:

const response = await ai.models.generateContent({
  model: 'gemini-2.5-flash',
  config: {
    systemInstruction: SYSTEM_PROMPT,  // TextBuddy's personality
    maxOutputTokens: 1024,
  },
  contents: geminiContents,  // Conversation history
});

Key Gemini-specific adaptations:

  1. System prompt goes in config.systemInstruction (not as a message)
  2. Conversation history uses role: 'model' instead of 'assistant'
  3. Message format uses parts: [{ text: '...' }] instead of plain content
  4. Response is accessed via response.text directly

System Prompt

The system prompt defines TextBuddy's entire personality and capabilities:

  • Core personality: Warm, casual, empathetic, honest, occasionally funny
  • Emotional handling: Acknowledges feelings first, asks follow-ups, never jumps to solutions
  • Reminder detection: Extracts reminders from natural language ("remind me to call mom tonight")
  • Check-in detection: Offers to follow up on mentioned future events
  • Safety: Directs crisis situations to appropriate resources (988 Lifeline, Crisis Text Line)
  • Response style: Conversational, 1-4 sentences for casual messages, longer for emotional ones

Getting a Free API Key

  1. Go to Google AI Studio
  2. Sign in with your Google account
  3. Click "Create API Key"
  4. Copy the key and paste it as GEMINI_API_KEY in your .env

Note: The free tier may use data sent through it to improve Google's products. For production use with sensitive conversations, use a paid tier.


Security Architecture

TextBuddy handles deeply personal, emotionally sensitive data. Security is not optional.

Message Encryption

  • All messages encrypted with AES-256-GCM before database storage
  • Each message has a unique initialization vector (IV) — no IV reuse
  • Auth tags stored alongside for tamper detection
  • Encryption key is a 32-byte hex value stored only in environment variables

Authentication

  • bcrypt password hashing with cost factor 12
  • JWT access tokens: 15-minute expiry (short-lived)
  • JWT refresh tokens: 30-day expiry (stored in client)
  • Automatic token refresh when access token expires

API Security

  • CORS: Locked to frontend domain only (not *)
  • Helmet: Security headers including Content Security Policy
  • Rate limiting: 60 requests/minute globally, 20/minute on AI endpoint
  • Input validation: Zod schemas on every route — no unvalidated input reaches the database
  • SQL injection: Prisma parameterized queries (automatic)

Privacy

  • Data isolation: Every query filters by userId — no cross-user access
  • Logging policy: NEVER logs message content, full emails, or JWT tokens
  • Account deletion: Cascade-deletes ALL user data (conversations, messages, reminders, sessions)
  • Data minimization: Only collects email, display name, timezone, device token

Notification Strategy

Notification Types

Type Trigger Example
Reminder User explicitly asked "hey, you wanted to call mom at 7pm"
Check-in User mentioned a future event "how'd the interview go? 👀"
Missed-day check-in No message in 48h "hey, been a minute. how are things?"
Morning greeting Optional, user opt-in "good morning ☀️ what's on your mind today?"

Anti-Spam Rules

  1. Maximum 3 notifications/day per user
  2. Quiet hours: Never send during 10pm–8am (configurable per user)
  3. Cooldown: Minimum 2 hours between unsolicited notifications
  4. User-initiated always go through: Explicit reminders bypass cooldowns
  5. Scheduled via cron polling: An external scheduler hits a secret-authed endpoint every minute, which fires any reminder whose scheduledAt has passed

API Reference

Method Route Auth Description
POST /auth/register No Create account
POST /auth/login No Get access + refresh tokens
POST /auth/refresh No Refresh expired access token
POST /messages/send Yes Send message, get AI reply
GET /messages/:convId Yes Load conversation history
GET /conversations Yes List user's conversations
DELETE /conversations/:id Yes Delete conversation + messages
GET /reminders Yes List upcoming reminders
DELETE /reminders/:id Yes Cancel a reminder
GET /users/me Yes Get current user profile
POST /users/fcm-token Yes Register push notification token
PUT /users/preferences Yes Update name, timezone, quiet hours
DELETE /users/me Yes Delete account + ALL data
GET /health No Server health check
POST /internal/cron/process-reminders Secret Fires due reminders — called by an external scheduler with Authorization: Bearer <CRON_SECRET>

Environment Variables

Backend (apps/api/.env)

Variable Required Description
NODE_ENV No development / production / test (default: development)
PORT No Server port (default: 3001)
FRONTEND_URL No Allowed CORS origin (default: http://localhost:5173)
DATABASE_URL Yes PostgreSQL connection string (pooled, e.g. Neon's pooled URL in production)
DIRECT_URL No Unpooled PostgreSQL connection string — used only by prisma migrate deploy (required in production with Neon; same as DATABASE_URL locally)
JWT_SECRET Yes 64-char hex string for signing access tokens
JWT_REFRESH_SECRET Yes 64-char hex string for signing refresh tokens (must differ from JWT_SECRET)
MESSAGE_ENCRYPTION_KEY Yes 64-char hex string (32 bytes) for AES-256-GCM message encryption
GEMINI_API_KEY Yes Google Gemini API key from AI Studio
FIREBASE_PROJECT_ID No Firebase project ID (needed for push notifications)
FIREBASE_PRIVATE_KEY No Firebase service account private key
FIREBASE_CLIENT_EMAIL No Firebase service account email
RATE_LIMIT_MAX No Max requests per window (default: 60)
RATE_LIMIT_WINDOW No Rate limit window in ms (default: 60000)
CRON_SECRET Yes 64-char hex string — bearer secret protecting POST /internal/cron/process-reminders

Frontend (apps/web/.env)

Variable Required Description
VITE_API_URL Yes Backend URL (default: http://localhost:3001)
VITE_FIREBASE_API_KEY No Firebase Web API key
VITE_FIREBASE_AUTH_DOMAIN No Firebase auth domain
VITE_FIREBASE_PROJECT_ID No Firebase project ID
VITE_FIREBASE_MESSAGING_SENDER_ID No Firebase messaging sender ID
VITE_FIREBASE_APP_ID No Firebase app ID
VITE_FIREBASE_VAPID_KEY No Firebase Web push VAPID key

Deployment

Local Development

# 1. Start databases
docker-compose up -d

# 2. Backend
cd apps/api
cp .env.example .env    # fill in your keys
npm install
npx prisma migrate dev
npm run dev             # starts on :3001

# 3. Frontend
cd apps/web
cp .env.example .env
npm install
npm run dev             # starts on :5173

Production (all on Vercel — two projects + Neon + a free external scheduler)

The backend runs as Vercel serverless functions (apps/api/api/index.ts wraps the Fastify app via app.ready() + app.server.emit('request', ...)). It's stateless by design — no in-process worker, no Redis — so it fits Vercel's free Hobby plan.

1. Database — Neon (Vercel Marketplace, free tier):

  1. From your Vercel dashboard, install the Neon integration (Marketplace → Neon)
  2. Create a database; copy the pooled connection string → DATABASE_URL and the direct/unpooled one → DIRECT_URL
  3. Run migrations once against Neon: DATABASE_URL=... DIRECT_URL=... npx prisma migrate deploy (from apps/api, with both URLs set to the Neon values)

2. Backend on Vercel:

  1. Create a new Vercel project, root directory apps/api (it already has its own vercel.json, which routes all requests to the function in api/index.ts)
  2. Set all backend env vars in the project's dashboard (DATABASE_URL, DIRECT_URL, JWT_SECRET, JWT_REFRESH_SECRET, MESSAGE_ENCRYPTION_KEY, GEMINI_API_KEY, CRON_SECRET, FRONTEND_URL → your frontend's Vercel URL, FIREBASE_* if used)
  3. Vercel auto-deploys on push to main; note the resulting https://<api>.vercel.app URL

3. Frontend on Vercel:

  1. Connect the repo as a second Vercel project using the existing root vercel.json (builds apps/web)
  2. Set VITE_API_URL to the backend project's URL from step 2
  3. Set VITE_FIREBASE_* variables (if using push notifications)
  4. Vercel auto-deploys on push to main

4. Reminder firing — free external scheduler: Vercel Hobby cron jobs run at most once a day, too infrequent for timely reminders. Instead, sign up for a free scheduler (e.g. cron-job.org) and configure it to call, every minute:

POST https://<api>.vercel.app/internal/cron/process-reminders
Authorization: Bearer <CRON_SECRET>

This fires any reminder whose scheduledAt has passed and marks it fired.


CI/CD

The GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request:

  1. Checkout code
  2. Setup Node.js 20
  3. Install dependencies (npm ci)
  4. Audit for high/critical vulnerabilities (npm audit)
  5. Type-check all TypeScript (npm run type-check)
  6. Lint code (npm run lint)

License

MIT


TextBuddy — Built with ❤️ and Gemini AI

About

A personal app, where you text about your day, create personal memories!

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors