Payload CMS Integration
Rendez-vous.ai uses Payload CMS as the foundational database layer and administrative backend. Instead of deploying a separate backend service just for database management, we embed Payload natively within our Next.js App Router.
This “code-first” approach means our database schema, authentication logic, and GraphQL API are all defined via TypeScript files within the rdv.ai-dashboard repository.
Why Payload CMS?
- Next.js Native: With Payload v3, the CMS runs directly inside the Next.js process. Admin routes, API endpoints (
/api/graphql,/api/users), and frontend pages share the same server environment. - Single Source of Truth: It manages the PostgreSQL database automatically. When you change a collection schema in code, Payload generates the exact Postgres migration required.
- Built-in Auth: Payload handles user sessions, JWT generation, and role-based access control (RBAC) out of the box.
- Auto-generated APIs: For every collection, Payload instantly generates REST and GraphQL APIs. Our Core API (
rdv.ai-api) relies heavily on this auto-generated GraphQL schema.
Core Collections (The Schema)
The database schema is defined in the collections/ directory. Each file exports a Payload CollectionConfig object. Here are the most critical collections driving the Rendez-vous.ai ecosystem:
👥 Users & Accounts
Users.ts: Manages authentication. It stores credentials, API keys, and links users to their specific roles (e.g.,admin,client).Accounts.ts(or Client/Tenant configs): Stores the business-level configuration for a client, such as their assigned phone numbers, AI agent prompts, business hours, and enabled tools.
📞 Conversations
These collections act as the logs for all interactions between external users and our AI agents.
Calls.ts: Records telephony metadata (caller ID, duration, status), the step-by-step transcript of the voice conversation, and links to the audio recording.Chats.ts/WebConversations.ts: Stores the message history for interactions originating from the embeddable web widget.
📅 Calendar & Bookings
Events.ts: Stores individual calendar events, either created by the AI or synced from external providers (like Nylas/Google Calendar).Bookings.ts: Manages the state of an appointment request, tying a user to anEvent.
📁 Media & Storage
Media.ts&Recordings.ts: These collections integrate directly with our S3-compatible storage (MinIO in local dev, AWS S3 in production). When the Core API uploads a call recording, Payload automatically pipes the file stream to S3 and saves the URL/metadata in PostgreSQL.
How Data is Accessed
Because Payload is the central database for a distributed system, data is accessed in two distinct ways depending on who is asking.
1. From the Dashboard Frontend (Local API)
When a user loads a page on the Next.js dashboard (e.g., viewing their Call History), we do not make HTTP requests to our own API. Instead, we use Payload’s Local API inside Next.js Server Components and Server Actions.
// Example inside a Next.js Server Action
import { getPayload } from 'payload';
import config from '@/payload.config';
export async function fetchCalls() {
const payload = await getPayload({ config });
// Directly queries Postgres, zero HTTP overhead
const calls = await payload.find({
collection: 'calls',
where: {
status: { equals: 'completed' }
}
});
return calls.docs;
}2. From the Core API (GraphQL / GenQL)
The rdv.ai-API service does not have direct access to the PostgreSQL database. Instead, it queries the Dashboard’s exposed GraphQL endpoint (/api/graphql).
To make this type-safe, we use GenQL. Whenever the Payload schema changes, we regenerate the GenQL client in the Core API repository, ensuring that our Node.js orchestration logic always perfectly matches the database schema.
// Example inside the Core API (rdv.ai-API)
import { createClient } from '../services/genql/genql-client';
const client = createClient({
url: process.env.PAYLOAD_URL + '/api/graphql',
headers: {
Authorization: `users API-Key ${process.env.PAYLOAD_API_KEY}`,
},
});
export async function fetchActiveAgents() {
const response = await client.query({
Accounts: {
docs: {
id: true,
telephony: {
active: true,
agentPrompt: true,
}
}
}
});
return response.Accounts.docs;
}