Backend
Learn the basics of your shared backend—Convex or Supabase—and how to work with the database.
Your VibeFast backend lives in packages/backend/. The structure depends on which backend you chose during setup.
Convex Backend
All your backend code lives in the packages/backend/convex/ directory. Convex provides a real-time database and serverless functions in one seamless package.
Queries, Mutations, and Actions
Convex functions are organized into three types:
| Type | Purpose | Can Modify Data? |
|---|---|---|
| Queries | Read data from the database. They are fast and automatically reactive. | No |
| Mutations | Write, update, or delete data. They are transactional and atomic. | Yes |
| Actions | Run server-side code with side effects, like calling third-party APIs. | Yes |
How-To: Add a New Field to a Table
Let's add a new isFavorite field to the recordings table.
Step 1: Modify the Schema
The single source of truth for your database is convex/schema.ts:
recordings: defineTable({
userId: v.id('users'),
name: v.optional(v.string()),
fileUri: v.string(),
duration: v.number(),
createdAt: v.number(),
status: v.union(v.literal('draft'), v.literal('completed')),
metering: v.optional(v.array(v.number())),
isFavorite: v.optional(v.boolean()), // <-- ADD THIS LINE
}).index('by_userId', ['userId']),Save the file and npx convex dev will automatically push the schema change.
Step 2: Create a Mutation
Add a new mutation in convex/recordingFunctions.ts:
export const toggleFavoriteStatus = mutation({
args: {
recordingId: v.id('recordings'),
isFavorite: v.boolean(),
},
handler: async (ctx, { recordingId, isFavorite }) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('Not authenticated');
const recording = await ctx.db.get(recordingId);
if (recording?.userId !== userId) throw new Error('Not authorized');
await ctx.db.patch(recordingId, { isFavorite });
},
});Step 3: Use in the Frontend
import { useMutation } from 'convex/react';
import { api } from 'convex/_generated/api';
const toggleFavorite = useMutation(api.recordingFunctions.toggleFavoriteStatus);
const handleFavoritePress = () => {
toggleFavorite({
recordingId: recording._id,
isFavorite: !recording.isFavorite,
});
};Supabase Backend
Your backend code lives in packages/backend/. It includes:
supabase/migrations/— SQL schema migrationssupabase/functions/— Edge Functions (Deno)src/services/— TypeScript service layer
Database + RLS
Supabase uses PostgreSQL with Row-Level Security. Queries run directly from the client but are filtered by RLS policies.
| Concept | Description |
|---|---|
| Tables | Defined in SQL migrations |
| RLS Policies | SQL rules that filter data per-user |
| Edge Functions | Deno functions for AI operations |
| Services | TypeScript wrappers for common operations |
How-To: Add a New Field to a Table
Let's add a is_favorite field to the recordings table.
Step 1: Create a Migration
Create a new migration file:
cd packages/backend
supabase migration new add_is_favorite_to_recordingsAdd the SQL:
ALTER TABLE recordings
ADD COLUMN is_favorite BOOLEAN DEFAULT FALSE;Apply it:
pnpm db:pushStep 2: Regenerate Types
pnpm db:generate-typesThis updates src/types/database.ts with the new column.
Step 3: Update the Service (Optional)
Add a helper function in src/services/recordings.ts:
export async function toggleFavorite(
supabase: SupabaseClient,
recordingId: string,
isFavorite: boolean
) {
const { error } = await supabase
.from('recordings')
.update({ is_favorite: isFavorite })
.eq('id', recordingId);
if (error) throw error;
}Step 4: Use in the Frontend
import { supabase } from '@/lib/supabase';
const handleFavoritePress = async () => {
await supabase
.from('recordings')
.update({ is_favorite: !recording.is_favorite })
.eq('id', recording.id);
};Found an issue or bug in the docs?
Help me improve! If you spot any errors, typos, or have suggestions, please let me know.
Reach out on X/Twitter @zafarbuildzz