A common shape: many users see each other live, everyone's posts persist, and something posts on a timer (an AI commenter, a digest, a heartbeat). This wires the four pieces end-to-end so you don't have to assemble them from four separate skills; each links to its full reference.
The pieces:
- Realtime for live presence + broadcasting new posts (app-realtime).
- A function + database so posts persist and survive reconnects (app-development).
- A scheduled workflow that calls a function on a cron to post automatically (workflow).
- Deploy that provisions all of it from
gipity.yaml(deploy).
1. Realtime: presence + a messages channel
Build on the @gipity/realtime kit (gipity add realtime) - don't hand-roll Colyseus. Use a presence channel for who's-here and a messages channel to broadcast new posts to everyone live:
import { createRealtime } from '@gipity/realtime';
const rt = await createRealtime({ room: 'main' });
const here = rt.channel('presence', { sync: 'presence' });
here.set({ name: myName }); // announce myself
here.onChange(peers => renderPresence(peers));
const feed = rt.channel('posts', { sync: 'messages' });
feed.onMessage(post => addPostToDom(post)); // live posts from anyone (incl. the scheduler)
Realtime is ephemeral - it makes posts appear live but does not store them. That's the database's job (next).
2. Persist posts with a function + DB
A public function writes the post and returns it; the client broadcasts it on the messages channel after a successful write. See app-development for the function/DB API.
// functions/add-post.js
export default async function (ctx, { db }) {
const { author, body } = ctx.body;
if (!body) return { error: 'body required' };
const r = await db.query(
'INSERT INTO posts (author, body) VALUES ($1, $2) RETURNING id, author, body, created_at',
[author ?? 'anon', body],
);
return { post: r.rows[0] };
}
// client: persist, then broadcast live
const { post } = await Gipity.fn('add-post', { author: myName, body: text });
feed.send(post);
3. A scheduled poster (the part with the trap)
To post automatically on a timer - an AI commenter, say - write a function that does the work, then drive it from a scheduled workflow with a function step. The scheduled function step gets the full project context, including the database, so db.query(...) works exactly as it does over HTTP:
// functions/post-ai-comment.js - reads recent posts, writes a new one
export default async function (ctx, { db }) {
const recent = await db.query('SELECT body FROM posts ORDER BY id DESC LIMIT 5', []);
const body = await generateComment(recent.rows); // your LLM call, etc.
const r = await db.query(
'INSERT INTO posts (author, body) VALUES ($1, $2) RETURNING id, author, body, created_at',
['ai', body],
);
return { post: r.rows[0] };
}
# workflows/post-ai-comment.yaml
name: ai-poster # unique within the project (enforced)
trigger: schedule # `cron` is an accepted alias
cron: "* * * * *" # every minute
steps:
- name: post
step_type: function
config: { name: post-ai-comment }
Use a function step, not a webhook hop. A scheduled step_type: function runs your function with db/secrets/fetch injected - the same services it gets over HTTP. (Earlier this wasn't true and people routed through a webhook to reach the DB; that workaround is no longer needed.)
To make the scheduled post show up live for connected users, broadcast it too - either have the client re-fetch, or add a tiny relay so the function's new row reaches the posts channel.
4. Deploy it all
One gipity deploy provisions the realtime room (declared by the kit), runs DB migrations, deploys the functions, and arms the workflow schedule. Declare the workflow phase in gipity.yaml (see deploy and workflow):
# gipity.yaml
version: 1
deploy:
phases:
- name: database
type: database
source: migrations
- name: functions
type: functions
source: functions
- name: workflows
type: workflows
source: workflows
Verify
gipity deploy- all phases green.gipity page inspect <dev-url>- page renders, console clean (see app-debugging).- Open two browsers - presence shows both; a post in one appears in the other.
gipity workflow runs ai-poster- showscompleted(not "No database configured"); a newaipost lands every minute.
Related skills
- app-realtime - rooms, channels, presence, the realtime kit
- app-development - functions, the
dbservice, gipity.yaml function permissions - workflow - triggers, scheduled function steps, YAML shape
- deploy - the deploy pipeline and gipity.yaml phases
- web-app-basics - frontend structure and patterns