Multi-tenant feedback intelligence platform — collect, analyze, and act on feedback at scale.
EchoDesk helps companies collect structured feedback from employees or customers, analyze it in real time, and get AI-generated summaries — without anyone reading thousands of responses by hand.
Think of it as a lightweight Typeform + Hotjar + Notion, built specifically for internal feedback workflows.
For form builders
- Drag-and-drop form editor with 7 question types — short text, long text, single choice, multiple choice, rating, NPS, and date
- Conditional required fields and option branching
- Anonymous submission mode — respondent identities are never stored
- One-click publish, pause, and archive
For analysts
- Real-time response dashboard with charts and breakdowns
- NPS scoring calculated automatically
- AI-powered summaries of open-text responses (powered by Groq / Llama 3)
- Export-ready data with pagination
For teams
- Multi-tenant workspaces — each company gets complete data isolation
- Role-based access control — Owner, Admin, Manager, Viewer
- Member invite system by email
- Slack digest integration — weekly summaries sent to any channel
For developers
- RESTful API with full Swagger documentation
- JWT authentication with Firebase Auth (email/password + Google)
- Rate limiting, request validation, and global error handling
- Full CI/CD pipeline with GitHub Actions
├── apps/
│ ├── web/ # Next.js 14 frontend → Vercel
│ │ └── src/
│ │ ├── app/ # App Router pages
│ │ ├── components/ # UI components
│ │ └── lib/ # Firebase, API client, Zustand stores
│ └── api/ # NestJS backend → Railway
│ └── src/
│ ├── modules/ # Feature modules (auth, forms, analytics...)
│ ├── common/ # Guards, decorators, filters
│ └── prisma/ # Database service
└── packages/
└── types/ # Shared TypeScript types
Browser → Next.js (Vercel)
↓
Firebase Auth → ID Token
↓
NestJS API (Railway) → JWT
↓
PostgreSQL (Neon) + Redis (Upstash)
↓
Groq API (AI summaries)
↓
Cloudinary (file storage)
↓
Slack API (notifications)
| Layer | Technology |
|---|---|
| Frontend | Next.js 14 (App Router), TypeScript, Tailwind CSS, ShadCN/UI |
| Backend | NestJS 10, TypeScript, Express |
| Database | PostgreSQL via Neon (serverless), Prisma ORM |
| Cache | Upstash Redis |
| Auth | Firebase Auth + JWT (RS256) |
| AI | Groq API — Llama 3 8B |
| File storage | Cloudinary |
| Notifications | Slack Block Kit API |
| State management | Zustand with persistence |
| Forms | React Hook Form + Zod validation |
| Charts | Recharts |
| Drag and drop | dnd-kit |
| CI/CD | GitHub Actions |
| Deployment | Vercel (frontend) + Railway (backend) |
| Monitoring | Sentry |
git clone https://github.com/yourusername/echodesk.git
cd echodesk
npm installcp .env.example .envFill in your credentials — see Environment Variables below.
Copy the env to each app:
cp .env apps/web/.env
cp .env apps/api/.envcd apps/api
npx prisma migrate dev --name init
npx prisma generate
cd ../..# Terminal 1 — frontend
cd apps/web && npm run dev
# Terminal 2 — backend
cd apps/api && npm run dev- Frontend: http://localhost:3000
- Backend: http://localhost:3001
- API Docs: http://localhost:3001/api/docs
Copy .env.example to .env and fill in:
# Database (Neon)
DATABASE_URL="postgresql://..."
# Redis (Upstash)
UPSTASH_REDIS_REST_URL="https://..."
UPSTASH_REDIS_REST_TOKEN="..."
# Firebase Admin (backend)
FIREBASE_PROJECT_ID="..."
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
FIREBASE_CLIENT_EMAIL="firebase-adminsdk-...@....iam.gserviceaccount.com"
JWT_SECRET="min-32-character-random-string"
# Firebase Client (frontend)
NEXT_PUBLIC_FIREBASE_API_KEY="AIza..."
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="....firebaseapp.com"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="..."
# AI
GROQ_API_KEY="gsk_..."
# File storage
CLOUDINARY_CLOUD_NAME="..."
CLOUDINARY_API_KEY="..."
CLOUDINARY_API_SECRET="..."
# Slack (optional)
SLACK_BOT_TOKEN="xoxb-..."
SLACK_SIGNING_SECRET="..."
# App
NEXT_PUBLIC_API_URL="http://localhost:3001/api/v1"Full interactive documentation available at /api/docs when running locally.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/exchange |
Exchange Firebase ID token for JWT |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/workspaces |
List user's workspaces |
| POST | /api/v1/workspaces |
Create workspace |
| GET | /api/v1/workspaces/:id/members |
List members |
| POST | /api/v1/workspaces/:id/members |
Invite member |
| PATCH | /api/v1/workspaces/:id/members/:userId/role |
Update role |
| DELETE | /api/v1/workspaces/:id/members/:userId |
Remove member |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/workspaces/:id/forms |
List forms |
| POST | /api/v1/workspaces/:id/forms |
Create form |
| PATCH | /api/v1/workspaces/:id/forms/:id/publish |
Publish form |
| PATCH | /api/v1/workspaces/:id/forms/:id/pause |
Pause form |
| GET | /api/v1/public/forms/:id |
Get active form (no auth) |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/responses |
Submit response (no auth) |
| GET | /api/v1/responses/form/:id |
Get paginated responses |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/analytics/forms/:id |
Get form analytics |
| GET | /api/v1/analytics/forms/:id/ai-summary |
Get AI summary |
npm install -g vercel
cd apps/web
vercel --prodAdd all NEXT_PUBLIC_* environment variables in the Vercel dashboard.
- Push to GitHub
- Create a new Railway project → "Deploy from GitHub repo"
- Select the
apps/apifolder as the root - Add all environment variables in Railway dashboard
- Railway auto-deploys on every push to
main
- Create a project at neon.tech
- Copy the connection string to
DATABASE_URL - Run
npx prisma migrate deployin production
├── modules/
│ ├── auth/ # Firebase token exchange, JWT strategy
│ ├── users/ # User profile management
│ ├── workspaces/ # Multi-tenant workspace + member management
│ ├── forms/ # Form CRUD, publish/pause/archive lifecycle
│ ├── responses/ # Response submission with validation
│ ├── analytics/ # Breakdown charts + Groq AI summaries
│ ├── uploads/ # Cloudinary avatar + logo uploads
│ └── notifications/ # Slack Block Kit digest sender
├── common/
│ ├── guards/ # JwtAuthGuard, RolesGuard
│ ├── decorators/ # @CurrentUser(), @Roles()
│ └── filters/ # Global HTTP exception filter
└── prisma/ # PrismaService (global singleton)
apps/web/src/
├── app/
│ ├── auth/ # Login + register pages
│ ├── dashboard/ # Overview, forms, team, settings
│ └── f/[formId]/ # Public form submission page
├── components/
│ ├── layout/ # Sidebar + Topbar
│ ├── forms/ # Form builder + renderer
│ └── shared/ # Empty states, spinners
└── lib/
├── firebase/ # Firebase client config
├── api/ # Axios client with interceptors
├── hooks/ # useAuth, useWorkspace
└── stores/ # Zustand workspace store
Why NestJS over Express? NestJS enforces a consistent module/service/controller pattern that scales cleanly as the codebase grows. Dependency injection, decorators, and the built-in Swagger plugin make it significantly more maintainable than plain Express for a project of this scope.
Why Neon over a self-hosted Postgres? Neon is serverless PostgreSQL with a generous free tier, automatic scaling, and zero ops. For a portfolio project it eliminates infrastructure management while demonstrating production-grade database usage with Prisma.
Why Zustand over Redux? Redux is significantly more boilerplate for the state complexity this app needs. Zustand's persist middleware handles localStorage sync in three lines, and its selective subscription model prevents unnecessary re-renders.
Why Groq over OpenAI? Groq's free tier is genuinely free with fast inference on Llama 3. For a feedback summarization use case the output quality is indistinguishable from GPT-3.5 and the API shape is nearly identical — easy to swap if needed.
MIT © Satvik Gahlot