# Recipe: Realtime Collaborative App with a Scheduler

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:
1. **Realtime** for live presence + broadcasting new posts ([app-realtime](app-realtime.md)).
2. **A function + database** so posts persist and survive reconnects ([app-development](app-development.md)).
3. **A scheduled workflow** that calls a function on a cron to post automatically ([workflow](workflow.md)).
4. **Deploy** that provisions all of it from `gipity.yaml` ([deploy](deploy.md)).

## 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:

```js
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](app-development.md) for the function/DB API.

```js
// 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] };
}
```

```js
// 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:

```js
// 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] };
}
```

```yaml
# 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](deploy.md) and [workflow](workflow.md)):

```yaml
# 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

1. `gipity deploy` - all phases green.
2. `gipity page inspect <dev-url>` - page renders, console clean (see [app-debugging](app-debugging.md)).
3. Open two browsers - presence shows both; a post in one appears in the other.
4. `gipity workflow runs ai-poster` - shows `completed` (not "No database configured"); a new `ai` post lands every minute.

## Related skills

- [app-realtime](app-realtime.md) - rooms, channels, presence, the realtime kit
- [app-development](app-development.md) - functions, the `db` service, gipity.yaml function permissions
- [workflow](workflow.md) - triggers, scheduled function steps, YAML shape
- [deploy](deploy.md) - the deploy pipeline and gipity.yaml phases
- [web-app-basics](web-app-basics.md) - frontend structure and patterns

