Skip to content

Repository files navigation

The Quiet Table

Turn eating alone into an opportunity for real human connection.

The Quiet Table matches solo diners at restaurants and cafes in real time. Signal you're open to company, the app finds someone nearby who did the same, and you share a meal. No social graph. No followers. No feed. Just a genuine moment with a stranger.

Built for Pearl Hacks.


Table of Contents


How It Works

The home screen offers two paths:

Path A — "I have something in mind"

  1. Search for and select a specific restaurant
  2. Set a time you'll be there
  3. Enter a waiting screen while the app looks for matches
  4. When a match is found, both users confirm
  5. A location hint appears — the matched user describes where they're sitting
  6. Icebreakers screen with audio + conversation prompts

Path B — "Open to anything"

  1. A map shows nearby solo diners at venues.
  2. Filter by preferences (age, gender)
  3. Tap a venue pin to see available diners, then tap "Match"
  4. On match confirmation — location hint and icebreakers follow

After the meal, the connection dissolves unless both users choose to exchange contact info.


Project Structure

quiet-table/
├── src/
│   ├── app/
│   │   ├── layout.tsx                 # Root layout (fonts, providers)
│   │   ├── page.tsx                   # Home screen (splash + two-path entry)
│   │   ├── globals.css                # Global styles + design tokens
│   │   ├── splash/page.tsx            # Splash screen (unauthenticated entry)
│   │   ├── login/page.tsx             # Google OAuth login
│   │   ├── onboarding/page.tsx        # 5-step profile setup
│   │   ├── map/page.tsx               # Nearby diners map (Path B)
│   │   ├── venue-picker/page.tsx      # Restaurant search + time picker (Path A)
│   │   ├── waiting/page.tsx           # "Waiting for matches..." (Path A)
│   │   ├── location-hint/page.tsx     # Matched user's seating description
│   │   ├── icebreakers/page.tsx       # Audio + conversation prompts
│   │   ├── profile/page.tsx           # User settings
│   │   └── api/auth/[...nextauth]/
│   │       └── route.ts               # NextAuth route handler
│   ├── components/
│   │   ├── AvatarButton.tsx
│   │   ├── Providers.tsx              # NextAuth session provider
│   │   └── ui/                        # Reusable UI components
│   ├── lib/
│   │   └── api.ts                     # Client-side API calls to backend
│   └── types/
│       └── next-auth.d.ts             # NextAuth type extensions
│
├── server/                            # Express backend
│   ├── index.ts                       # Entry point
│   ├── config/db.ts                   # MongoDB connection
│   ├── middleware/auth.ts             # JWT auth middleware
│   ├── routes/
│   │   ├── users.ts                   # Auth, registration, profile
│   │   ├── sessions.ts               # Availability signals + matching
│   │   └── icebreakers.ts             # Gemini API calls
│   ├── models/
│   │   ├── User.ts
│   │   ├── Session.ts
│   │   ├── Match.ts
│   │   └── Venue.ts
│   ├── services/
│   │   ├── gemini.ts                  # Icebreaker generation
│   │   └── bandwidth.ts              # SMS verification
│   └── scripts/seed.ts               # Database seeding
│
├── shared/
│   └── types.ts                       # Shared TypeScript types
│
├── CLAUDE.md
└── .env

Tech Stack

Layer Technology Purpose
Frontend Next.js 16 (React 19) Web app with App Router
Styling Tailwind CSS 4 Utility-first styling
Auth NextAuth.js v5 (beta) Google OAuth on the frontend
Backend Node.js + Express API server
Database MongoDB Atlas + Mongoose Users, sessions, matches, venues
Maps Google Maps API + Places Map view and venue search
AI Gemini API Icebreaker generation
Audio ElevenLabs Spoken icebreaker (optional)
SMS Bandwidth Phone verification + notifications

Data Models

User

{
  _id: ObjectId,
  phone: string,              // Verified via Bandwidth SMS OTP
  alias: string,              // Display name shown to other users
  legalName: string,          // Private — never surfaced to other users
  preferences: {
    openTo: "1-on-1" | "group" | "both",
    starterOnly: boolean,
  },
  flags: number,              // Silent report counter
  active: boolean,
  createdAt: Date
}

Session (an availability signal)

{
  _id: ObjectId,
  userId: ObjectId,
  venueId: ObjectId,
  location: {
    type: "Point",
    coordinates: [lng, lat]
  },
  status: "available" | "matched" | "ended",
  mode: "specific" | "open",
  openTo: "1-on-1" | "group",
  starterOnly: boolean,
  locationHint?: string,
  scheduledTime?: Date,
  expiresAt: Date,
  createdAt: Date
}

Match

{
  _id: ObjectId,
  sessionIds: ObjectId[],
  userIds: ObjectId[],
  venueId: ObjectId,
  icebreaker: string,
  icebreakerAudioUrl?: string,
  status: "pending" | "confirmed" | "active" | "ended",
  contactExchanged: boolean[],
  createdAt: Date,
  endedAt?: Date
}

Venue

{
  _id: ObjectId,
  name: string,
  address: string,
  location: { type: "Point", coordinates: [lng, lat] },
  partnerStatus: "active" | "inactive",
  staffNotified: boolean,
  createdAt: Date
}

Backend API

Base URL: http://localhost:3001/api

Users

Method Endpoint Description
POST /users/register Register with phone number, trigger SMS OTP
POST /users/verify Verify OTP, receive JWT
GET /users/me Get current user profile
PATCH /users/me Update alias, preferences
POST /users/report/:userId Silently flag another user

Sessions

Method Endpoint Description
POST /sessions Create a new availability signal
GET /sessions/nearby Get nearby available sessions
PATCH /sessions/:id/join Express interest in joining a session
PATCH /sessions/:id/end End your current session

Matches

Method Endpoint Description
GET /matches/:id Get match details including icebreaker
PATCH /matches/:id/exchange Opt in to contact exchange
PATCH /matches/:id/end End the match session

Matching Algorithm

Located in server/services/matching.ts.

  1. User creates a Session with their current location at a venue
  2. Backend queries MongoDB for other active sessions within 500m using $nearSphere
  3. Candidates are filtered by compatibility and status
  4. Candidates appear on the map as available diners
  5. Mutual match required — both users must opt in before either sees the other's alias
  6. On mutual confirmation, a Match document is created and icebreaker generation triggers

Required MongoDB indexes:

db.sessions.createIndex({ location: "2dsphere" })
db.venues.createIndex({ location: "2dsphere" })
db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })

Icebreaker Generation

Located in server/services/gemini.ts. When a match is confirmed, the Gemini API generates a single warm, curious conversation starter based on the venue context — one genuine question, not a list.

If ELEVENLABS_API_KEY is set, the icebreaker is also converted to audio via ElevenLabs.


Environment Variables

Create .env files in both the project root and server/. Never commit these.

Root .env.local (Next.js)

NEXTAUTH_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=
NEXT_PUBLIC_BACKEND_URL=http://localhost:3001

server/.env

MONGODB_URI=
GEMINI_API_KEY=
ELEVENLABS_API_KEY=
ELEVENLABS_VOICE_ID=
BANDWIDTH_ACCOUNT_ID=
BANDWIDTH_USERNAME=
BANDWIDTH_PASSWORD=
BANDWIDTH_FROM_NUMBER=
JWT_SECRET=
PORT=3001

Getting Started

Prerequisites

  • Node.js 18+
  • MongoDB Atlas account (free tier works)
  • Google Cloud project with Maps API + OAuth credentials
  • Gemini API key

Install & Run

# Clone the repo
git clone https://github.com/your-org/quiet-table.git
cd quiet-table

# Install frontend dependencies
npm install

# Install backend dependencies
cd server && npm install && cd ..

# Add your .env.local (root) and server/.env (see above)

# Start the backend (in one terminal)
cd server && npx ts-node-dev index.ts

# Start the frontend (in another terminal)
npm run dev

The frontend runs at http://localhost:3000 and the backend at http://localhost:3001.


Safety Principles

Safety is a design constraint, not a feature:

  • No location data retained — coordinates are used for matching only, not logged after a session ends
  • Alias only — legal name is never included in any API response to another user
  • Mutual match required — neither user sees the other's alias until both opt in
  • Silent flagging — reports increment a counter; removal is automatic at threshold
  • Session TTL — all sessions expire after 90 minutes via MongoDB TTL index
  • Public venues only — session creation is rejected at unverified venues
  • Rate limiting — max 3 active sessions per day per user

Built at Pearl Hacks

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages