A lightweight, full-stack CRM built for small businesses β no signup friction, just sign in with Google and get to work.
Redberry CRM is a production-grade contact management system designed for small businesses that need a fast, no-friction tool to manage leads and customer relationships. Rather than onboarding users through email signup flows, Redberry uses Google OAuth β zero friction, zero forgotten passwords.
The application is fully self-contained: a Next.js monorepo that handles both the frontend React application and the backend API routes, backed by MongoDB Atlas and deployed to Vercel.
Screenshots coming soon β contributions welcome.
| Overview Dashboard | Contacts β Table View | Contacts β Board View |
|---|---|---|
![]() |
![]() |
![]() |
Redberry follows a full-stack monorepo architecture using Next.js App Router. There is no separate backend service β all API logic lives in /src/app/api as Next.js Route Handlers, co-located with the frontend. This eliminates network round-trips between services and simplifies deployment to a single Vercel project.
graph TD
subgraph Client["Browser (React)"]
UI["UI Components\n(NextUI + Tailwind)"]
RQ["React Query\n(Server State Cache)"]
ZS["Zustand\n(Client State)"]
UI --> RQ
UI --> ZS
end
subgraph NextJS["Next.js App Router (Vercel)"]
Pages["Page Components\n/src/app/*/page.tsx"]
Routes["API Route Handlers\n/src/app/api/*"]
Middleware["JWT Middleware\nverifyToken.js"]
Pages --> Routes
Routes --> Middleware
end
subgraph Services["External Services"]
MongoDB["MongoDB Atlas\n(contacts, notes, users)"]
Firebase["Firebase Auth\n(Google OAuth)"]
Twilio["Twilio\n(SMS)"]
Mailchimp["Mailchimp Transactional\n(Email)"]
OpenAI["OpenAI API\n(Task Prioritization)"]
Sentry["Sentry\n(Error Tracking)"]
end
RQ -->|Axios /api/*| Routes
Middleware -->|Verified| MongoDB
Routes --> Twilio
Routes --> Mailchimp
Routes --> OpenAI
Client -->|Sign In| Firebase
Firebase -->|ID Token| ZS
NextJS --> Sentry
- The user authenticates via Google through Firebase. Firebase returns a signed ID token.
- The ID token is stored in Zustand and injected as a
Bearerheader on every outbound request via the Axios HTTP client (/src/services/http.ts). - Protected API routes pass the token through
verifyToken.js(Google Auth Library) before hitting MongoDB. - React Query manages all server state β caching, background refetching, and optimistic updates β so the UI never blocks on a network call.
Every dependency here was chosen deliberately. The table below explains the why, not just the what.
| Layer | Technology | Rationale |
|---|---|---|
| Framework | Next.js 15 (App Router) | Co-locating API routes with the frontend eliminates a separate backend service. App Router's React Server Components reduce client bundle size for static content. |
| Language | TypeScript 5 | Strict mode enabled throughout. Shared types between frontend components and API route handlers prevent contract drift without a separate type-generation step. |
| UI Components | NextUI v2 + Ant Design v5 | NextUI covers the primary component set (inputs, selects, navbars) with first-class Tailwind integration. Ant Design fills gaps (complex data tables, date pickers) where battle-tested primitives save time. |
| Styling | Tailwind CSS v3 + Styled Components v6 | Tailwind handles layout and utility classes at development speed. Styled Components handles dynamic theming (dark mode) where CSS-in-JS is genuinely more expressive. |
| Server State | React Query v3 | Separates server state from UI state entirely. Optimistic updates are implemented at the mutation level, keeping component code clean. Devtools in development accelerate debugging cache behaviour. |
| Client State | Zustand v4 | Minimal API, no boilerplate, works outside of React components (useful for the Axios interceptor pattern). Used strictly for UI state (current user, modal visibility, active navigation). |
| Forms | React Hook Form v7 + Zod v3 | Uncontrolled form inputs via RHF keep re-renders minimal. Zod schemas are the single source of truth for validation β the same schema is used client-side (via @hookform/resolvers) and server-side (API route validation). |
| Database | MongoDB Atlas (Serverless) | Schema-flexible enough to accommodate evolving CRM data models without migrations. Atlas Serverless scales to zero, appropriate for a product at this stage. |
| Authentication | Firebase Auth + Google OAuth | Delegates credential management entirely. Token verification server-side uses Google Auth Library β stateless, no session store required. |
| Mailchimp Transactional (Mandrill) | Reliable transactional delivery with open/click tracking, appropriate for CRM-originated emails. | |
| SMS | Twilio | Industry-standard SMS API with a straightforward Node.js SDK. |
| AI | OpenAI API (GPT-3.5-turbo) | Powers the dashboard task prioritization feature. GPT-3.5-turbo is cost-effective for classification and prioritization tasks that don't require deep reasoning. |
| Error Tracking | Sentry | Integrated at the Next.js config level with source map upload. Client-side errors are annotated with React component names automatically. |
| Deployment | Vercel | Zero-config for Next.js, preview deployments per PR, edge network for static assets. |
Decision: All API logic lives as Next.js Route Handlers inside /src/app/api/, co-located with the frontend.
Tradeoff: This removes the operational overhead of running, deploying, and versioning a separate backend service. The downside is that all backend code runs inside a serverless function context β long-running tasks (e.g., bulk email sends) are not suitable here without an external queue.
Decision: All mutations (create, update, delete) implement optimistic cache updates in the onMutate callback before the server confirms the operation.
// /src/app/Contacts/page.tsx (pattern)
const deleteMutation = useMutation((id: string) => deleteContact(id), {
onMutate: async (id) => {
await queryClient.cancelQueries('contacts');
const previous = queryClient.getQueryData('contacts');
queryClient.setQueryData('contacts', (old: any) =>
old.filter((item: any) => item._id !== id)
);
return { previous };
},
onError: (_err, _id, context) => {
queryClient.setQueryData('contacts', context?.previous);
},
onSuccess: () => queryClient.invalidateQueries('contacts'),
});Tradeoff: The UI feels instant. The rollback-on-error path handles the failure case. This pattern makes the code slightly more verbose per mutation but delivers a meaningfully better user experience on high-latency connections.
Decision: Zod schemas in /src/types.ts and /src/app/api/apiTypes.ts are shared between client-side form validation (via @hookform/resolvers/zod) and server-side API route validation.
Tradeoff: A single schema change propagates to both layers automatically, eliminating drift. The tradeoff is a slightly heavier client bundle (Zod is ~14kb gzipped), which is acceptable given its role in form UX.
Decision: Firebase ID tokens are verified server-side using the Google Auth Library on every request. There is no session table or Redis store.
Tradeoff: No server-side state to manage or expire. Every request is independently verifiable. The tradeoff is that token revocation requires waiting for the token's natural expiry (~1 hour) β acceptable for a CRM with no immediate account-termination requirement.
Decision: The Contacts page renders either a sortable data table (ContactTableView) or a Kanban board (ContactBoardView) based on a view toggle. Both views share the same React Query cache and optimistic mutation logic.
Tradeoff: Two maintained view components add complexity. The benefit is that users with different working styles (list-oriented vs. pipeline-oriented) can operate the same underlying data model in the way that fits them.
- Node.js >= 18.17.0
- A MongoDB Atlas account (free tier works)
- A Firebase project with Google Sign-In enabled
- Accounts for: Twilio, Mailchimp Transactional, OpenAI (optional β for AI features)
git clone https://github.com/DavidMarom/redberry-crm.git
cd redberry-crm
npm installCreate a .env.local file in the project root:
# MongoDB
MONGODB_URI=mongodb+srv://<user>:<password>@cluster.mongodb.net/rb
# Firebase (Client-side)
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
# Twilio
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=+1xxxxxxxxxx
# Mailchimp Transactional (Mandrill)
MAILCHIMP_API_KEY=
# OpenAI
OPENAI_API_KEY=
# Sentry (optional β remove from next.config.js to disable)
SENTRY_AUTH_TOKEN=Note: Firebase credentials prefixed with
NEXT_PUBLIC_are exposed to the browser. These are safe to expose β Firebase Security Rules and the server-side token verification layer protect actual data access.
npm run devOpen http://localhost:3000. Sign in with your Google account β the app will create your user record on first login and send a welcome email if Mailchimp is configured.
npm run build
npm startredberry-crm/
βββ src/
β βββ app/
β β βββ api/ # Next.js Route Handlers (backend)
β β β βββ contacts/ # Contact CRUD + [owner] dynamic route
β β β βββ notes/ # Notes CRUD + [owner] dynamic route
β β β βββ users/ # User management
β β β βββ sms/ # Twilio SMS
β β β βββ send-mail/ # Mailchimp transactional email
β β β βββ openai/ # AI task prioritization
β β β βββ apiTypes.ts # Zod schemas for API validation
β β βββ Contacts/ # Contacts page + Table/Board views
β β βββ Notes/ # Notes page
β β βββ Email/ # Email composition page
β β βββ Overview/ # Dashboard + charts + AI recommendations
β β βββ Settings/ # User profile settings
β β βββ layout.tsx # Root layout (providers, navigation)
β β βββ page.tsx # Home β renders Overview
β βββ components/ # Shared UI components
β β βββ SideBar/ # Responsive navigation sidebar
β β βββ Header/ # Top bar with user profile
β β βββ LandingPage/ # Unauthenticated landing page
β βββ services/ # Service layer (API clients, MongoDB, auth)
β β βββ http.ts # Axios instance (base URL, auth header)
β β βββ contacts.ts # Contact API functions
β β βββ notes.ts # Notes API functions
β β βββ users.ts # User API functions
β β βββ mongo.ts # MongoDB connection + operations
β β βββ auth.ts # Google OAuth handlers
β β βββ sms.ts # SMS service
β β βββ mailchimp.ts # Email service
β β βββ openai.ts # AI service
β β βββ middlewares/
β β βββ verifyToken.js # JWT verification middleware
β βββ store/ # Zustand stores
β β βββ user.js # Auth state, user profile
β β βββ contacts.js # Contact edit state
β β βββ popup.js # Modal visibility
β β βββ navigation.js # Active route state
β βββ utils/ # Pure utility functions
β β βββ utils.ts # localStorage helpers, text truncation
β β βββ userUtils.ts # Sign-in / sign-out orchestration
β β βββ contactsUtils.ts # Contact-specific helpers
β βββ types.ts # Zod schemas + inferred TypeScript types
βββ public/ # Static assets
βββ next.config.js # Next.js + Sentry config
βββ tailwind.config.ts # Tailwind theme + NextUI plugin
βββ tsconfig.json # TypeScript (strict mode, @/* path alias)
Full CRUD for contacts with per-user data isolation (owner field tied to Firebase UID). Supports custom status values, phone, email, and freeform notes per contact.
Technically: Contacts are stored in MongoDB with the user's Firebase UID as the owner field. All API routes under /api/contacts/[owner] return only records matching the authenticated user's UID, enforced server-side.
Switch between a sortable data table and a Kanban board grouped by contact status. Both views share the same React Query cache β switching view does not re-fetch.
Technically: View state is local to the Contacts page component. The React Query query key "contacts" is shared between both view components, so the data is hydrated once and rendered differently.
Compose and send transactional emails to contacts directly from the CRM. Built with React Hook Form + Zod validation for the composition form, routed through the /api/send-mail handler to Mailchimp Transactional (Mandrill).
Send an SMS to any contact with a phone number on file. The /api/sms route dispatches through Twilio's REST API using server-side credentials β the Twilio auth token is never exposed to the browser.
The Overview dashboard includes an AI recommendations panel that sends the user's current contact list to the /api/openai route. GPT-3.5-turbo analyzes contact statuses and returns prioritized action suggestions.
Technically: The prompt is constructed server-side from the user's live contact data. Responses are streamed back and rendered in the dashboard panel. This feature degrades gracefully if the OpenAI API key is not configured.
A freeform notes board scoped to the authenticated user. Notes are stored in MongoDB and protected by JWT middleware β the /api/notes route requires a valid Bearer token, unlike public contact routes.
The Overview page renders two charts built with Recharts: a contact growth timeline (Graph01) and a status distribution pie chart (PieChart). Both are computed client-side from the React Query contacts cache β no separate analytics API call.
One-click sign-in with Google. Firebase handles the OAuth flow, returns a signed ID token, which is verified server-side on protected routes using the Google Auth Library. A welcome email is dispatched on first login.
Full dark mode support via next-themes. Theme preference is persisted in localStorage and applied at the <html> level to prevent flash on load. The Tailwind config includes dark mode class variants throughout.
Two sidebar variants render based on viewport: a full text-and-icon sidebar on desktop and an icon-only compact sidebar on mobile. Navigation active state is managed in Zustand and syncs with the Next.js router.
The following features are planned. Contributions are welcome β see Contributing below.
- Bulk actions β select multiple contacts for batch status update, email, or delete
- Contact import/export β CSV import for migrating from other CRMs, CSV/JSON export
- Activity timeline β per-contact log of emails sent, SMS sent, notes added, status changes
- Pipeline stages β customizable Kanban columns (beyond the current status field)
- Team accounts β multi-user workspaces with role-based access (owner / editor / viewer)
- Contact tagging β free-form tags for filtering and segmentation beyond status
- Reminders & follow-ups β schedule a follow-up action on a contact with a notification
- Email templates β save and reuse email compositions
- Real-time collaboration β live presence and cursor tracking via Ably (infrastructure partially in place)
- Test coverage β Jest + React Testing Library for unit tests; Playwright for E2E
For each ticket or feature, create a branch from main using the following convention:
yourFirstName/RED-#ticketNumber
# Example: david/RED-47
- Fork the repository and create your branch
- Make your changes
- Run the linter before opening a PR:
npm run lint
- Open a pull request against
mainwith a clear description of what changed and why
- TypeScript strict mode is enforced β no
anytypes without an explicit comment explaining why - Zod schemas are the canonical source for any data shape β add new schemas to
/src/types.ts(frontend models) or/src/app/api/apiTypes.ts(API payloads) before implementing - React Query for all server state β do not use
useState+useEffectfor data fetching - Zustand for UI state only β authentication state, modal visibility, active navigation
- Service functions live in
/src/services/β components should not callaxiosorfetchdirectly - Validate API request bodies at the route handler level using the Zod schemas in
apiTypes.ts
Open an issue on GitHub with:
- A clear description of the bug or feature request
- Steps to reproduce (for bugs)
- Expected vs. actual behaviour
MIT Β© David Marom


