Your personal AI text companion. Talk about your day, set reminders, get check-ins — like texting a friend who's always there.
- What Is TextBuddy?
- Tech Stack
- Repository Structure
- Getting Started
- Backend Architecture
- Frontend Architecture
- AI Integration (Gemini)
- Security Architecture
- Notification Strategy
- API Reference
- Environment Variables
- Deployment
- CI/CD
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.
- User sends a message (text)
- Gemini processes it with full conversation history + user profile
- Gemini replies empathetically and helpfully
- If a reminder/task is detected, it's scheduled via the notification engine
- Notifications fire at the right moment — not spam, but felt
TextBuddy should feel like the most attentive, non-judgmental friend you've ever had. Never clinical. Never robotic. Always warm.
| 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 |
| 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 |
| 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) |
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
| 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 |
- Node.js 20+ — Download
- Docker — Download (for local Postgres)
- Google Gemini API Key (free) — Get one at Google AI Studio
- Firebase Project (optional for push notifications) — see Firebase Setup below
git clone https://github.com/YOUR_USERNAME/textbuddy.git
cd textbuddy
npm installdocker-compose up -dThis starts PostgreSQL on localhost:5432 (user: textbuddy, password: password, db: textbuddy).
cd apps/api
cp .env.example .envEdit 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).
cd apps/api
npx prisma migrate dev --name initThis creates all the database tables (users, conversations, messages, reminders, sessions).
cd apps/web
cp .env.example .envThe default VITE_API_URL=http://localhost:3001 is already correct for local dev.
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 devGo to http://localhost:5173 in your browser.
- You'll see the Welcome screen — click "Get Started"
- Register a new account (email + password, minimum 10 characters)
- Start chatting with TextBuddy! 🎉
Push notifications require a Firebase project. You can skip this for initial testing — the app works without push notifications.
To set up push notifications:
- Go to Firebase Console
- Create a new project (or use an existing one)
- Enable Cloud Messaging in the project settings
- Generate a new Web push certificate (this gives you the VAPID key)
- Create a Service Account and download the JSON credentials
- Add the Firebase config to both
.envfiles:- 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 (
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.
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 (
assistantrole) to Gemini format (modelrole, withpartsarray)
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
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)
Handles push notification delivery via Firebase Cloud Messaging (FCM).
- Schedule reminder: Creates a database record with a
scheduledAttimestamp — 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)
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: RequiresAuthorization: Bearer <CRON_SECRET>. Finds all reminders wherescheduledAt <= nowandfired = false, sends the push notification for each, and marks themfired. An external scheduler (e.g. cron-job.org, free) calls this every minute — see Deployment.
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)
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)
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
- Strips HTML tags from user input
- Collapses whitespace while preserving newlines
- Masks email addresses for safe logging (e.g.,
joh***@example.com)
- 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
- Welcome Screen → First-time visitors see an animated onboarding page explaining what TextBuddy does
- Login/Register → Email + password authentication (auto-detects user's timezone on registration)
- Chat Interface → The main experience — full-screen mobile-first chat with TextBuddy
- 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
- 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)
- Three bouncing dots with staggered animation
- Spring-based entrance/exit animations via Framer Motion
- 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)
- 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
- Animated logo entrance (spring + rotation)
- Staggered feature cards with icons
- Gradient background with floating ambient orbs
- "Get Started" button transitions to auth
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
- 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
- Wraps
fetchwith 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
- 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)
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
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:
- System prompt goes in
config.systemInstruction(not as a message) - Conversation history uses
role: 'model'instead of'assistant' - Message format uses
parts: [{ text: '...' }]instead of plaincontent - Response is accessed via
response.textdirectly
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
- Go to Google AI Studio
- Sign in with your Google account
- Click "Create API Key"
- Copy the key and paste it as
GEMINI_API_KEYin 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.
TextBuddy handles deeply personal, emotionally sensitive data. Security is not optional.
- 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
- 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
- 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)
- 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
| 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?" |
- Maximum 3 notifications/day per user
- Quiet hours: Never send during 10pm–8am (configurable per user)
- Cooldown: Minimum 2 hours between unsolicited notifications
- User-initiated always go through: Explicit reminders bypass cooldowns
- Scheduled via cron polling: An external scheduler hits a secret-authed endpoint every minute, which fires any reminder whose
scheduledAthas passed
| 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> |
| 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 |
| 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 |
# 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 :5173The 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):
- From your Vercel dashboard, install the Neon integration (Marketplace → Neon)
- Create a database; copy the pooled connection string →
DATABASE_URLand the direct/unpooled one →DIRECT_URL - Run migrations once against Neon:
DATABASE_URL=... DIRECT_URL=... npx prisma migrate deploy(fromapps/api, with both URLs set to the Neon values)
2. Backend on Vercel:
- Create a new Vercel project, root directory
apps/api(it already has its ownvercel.json, which routes all requests to the function inapi/index.ts) - 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) - Vercel auto-deploys on push to
main; note the resultinghttps://<api>.vercel.appURL
3. Frontend on Vercel:
- Connect the repo as a second Vercel project using the existing root
vercel.json(buildsapps/web) - Set
VITE_API_URLto the backend project's URL from step 2 - Set
VITE_FIREBASE_*variables (if using push notifications) - 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.
The GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request:
- Checkout code
- Setup Node.js 20
- Install dependencies (
npm ci) - Audit for high/critical vulnerabilities (
npm audit) - Type-check all TypeScript (
npm run type-check) - Lint code (
npm run lint)
MIT
TextBuddy — Built with ❤️ and Gemini AI