Skip to content

Realtime chat

Chat for a React app built with Vite. src/lib/vaif.ts has the client and the helpers for a messages table and a profiles table: sendMessage posts as the signed-in user, getMessages loads a channel’s messages, and setDisplayName sets the user’s name. A message carries no name of its own: each one shows its author’s profile name, so nobody can post under someone else’s. src/hooks/useRealtimeMessages.ts loads a channel’s messages, then subscribes to the table’s row changes for that channel, so new, edited and deleted messages show up without a reload.

Terminal window
vaif init --template realtime-chat

Run it in your app's folder. It writes vaif.config.json and the template's files there. It stops if the folder already has a vaif.config.json, and leaves any other file that already exists alone; --force overwrites both.

It asks which features to include. This template has files for Database and Functions; Database is selected to start with. To skip the question, name them: vaif init --template realtime-chat --features database,functions. Add one later with vaif init --template realtime-chat --add-features <feature>.

FileWritten
src/lib/vaif.tsAlways
src/hooks/useRealtimeMessages.tsAlways
drizzle/0002_messages.sqlAlways
.env.exampleAlways
README.mdAlways
drizzle/0001_initial.sqlWith Database
drizzle.config.tsWith Database
src/db/schema.tsWith Database
functions/hello.tsWith Functions

This template adds files to an app you already have, and writes no package.json. Install the packages its files import:

Terminal window
npm install @vaifstudio/client @vaifstudio/react

Copy .env.example to .env and fill it in. The project ID and the keys are on Security > Credentials in the console.

VariableValue
VITE_VAIF_API_URLThe API address. Keep the value in the file, https://api.vaif.studio.
VITE_VAIF_PROJECT_IDYour project ID, for the app.
VITE_VAIF_PUBLISHABLE_KEYThe project's publishable key (vk_pub_). Row rules apply to it, so it is safe in an app.

The template writes two SQL files for your database, applied in name order. The comment line above each CREATE TABLE sets that table's row rules.

drizzle/0001_initial.sql, written with the Database feature, is an example: edit it to describe your own tables before you apply it.

drizzle/0001_initial.sql
-- Initial migration. Customize this schema for your project; vaif db push
-- applies it. The comment directly above each CREATE TABLE sets that table's
-- row rules: here each signed-in user reads and changes only their own rows,
-- and the owner column is filled in from their sign-in on insert. Keep a rule
-- comment on every table you add: a table with no row rules is closed to
-- signed-out visitors but open to every signed-in user.
-- vaif:owner id
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"email" text NOT NULL UNIQUE,
"name" text,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- vaif:owner user_id
CREATE TABLE IF NOT EXISTS "posts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"user_id" uuid NOT NULL REFERENCES "users"("id"),
"title" text NOT NULL,
"content" text,
"published" boolean NOT NULL DEFAULT false,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
  • users (-- vaif:owner id): each signed-in user reads and changes only their own rows, and id is filled in from their sign-in.
  • posts (-- vaif:owner user_id): each signed-in user reads and changes only their own rows, and user_id is filled in from their sign-in.

drizzle/0002_messages.sql, written every time:

drizzle/0002_messages.sql
-- Profiles and chat messages, applied by vaif db push. Every signed-in user
-- reads every row; each changes only their own, and the owner column (id,
-- user_id) is filled in from their sign-in on insert. Signed-out visitors read
-- nothing. A message has no name of its own: readers show the author's
-- profile name, so nobody can post under someone else's.
-- vaif:owner-writes id
CREATE TABLE IF NOT EXISTS "profiles" (
"id" uuid PRIMARY KEY,
"display_name" text NOT NULL UNIQUE,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- vaif:owner-writes user_id
CREATE TABLE IF NOT EXISTS "messages" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"user_id" uuid NOT NULL,
"channel_id" text NOT NULL,
"content" text NOT NULL,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS "messages_channel_created_idx" ON "messages" ("channel_id", "created_at");
  • profiles (-- vaif:owner-writes id): every signed-in user reads every row; each changes only their own, and id is filled in from their sign-in.
  • messages (-- vaif:owner-writes user_id): every signed-in user reads every row; each changes only their own, and user_id is filled in from their sign-in.

Put your project ID in vaif.config.json ("projectId"), sign in, and apply them. With no --dir, vaif db push finds the template's SQL folder, or a ./migrations folder, and applies its .sql files in name order.

Terminal window
vaif login
vaif db push --dry-run # what each file would change
vaif db push

Then turn realtime on for messages, under Realtime > Tables in the console. Only tables with realtime on send changes. See Realtime overview.

Sign the user in before sending: a visitor who is not signed in can neither read nor write these tables, and receives no realtime events from them.