# Posts

> Forum and feedback boards with voted posts, roadmap statuses, merged duplicates, replies and moderation, on the messaging core.

Source: https://bettersupabase.com/docs/blocks/posts

The `posts` block keeps boards of posts per organization: a `forum` board
for questions and discussion, or a `feedback` board where members post ideas
and vote on them. Posts take up and down votes through
[reactions](/docs/blocks/reactions), replies through
[comments](/docs/blocks/comments) on the [messaging](/docs/blocks/messaging)
core, and reports, hiding and bans through
[moderation](/docs/blocks/moderation). Moderators set a roadmap status, pin,
lock and merge duplicates.

```bash
better-supabase add posts   # adds tenant, access, subjects, messaging, comments, reactions and moderation
```

| Permission       | Lets a member                                    | Default roles       |
| ---------------- | ------------------------------------------------ | ------------------- |
| `posts.read`     | read boards and posts, and vote                  | `member`, `admin`   |
| `posts.write`    | write posts and edit or delete their own         | `member`, `admin`   |
| `posts.moderate` | set statuses, pin, lock, merge and edit any post | `admin` (`posts.*`) |
| `posts.manage`   | create, rename and archive boards                | `admin` (`posts.*`) |

The example app's [post board](/docs/registry/ui-recipes/post-board) recipe
shows a feedback board's posts by votes, with an upvote toggle and a New
post dialog.

## Boards and posts [#boards-and-posts]

```ts
import { createPosts, rpcTransport } from "better-supabase/blocks/posts";

const posts = createPosts({
  transport: rpcTransport(supabase),
  tenant: organizationId,
});

const ideas = await posts
  .saveBoard({ key: "ideas", name: "Ideas", kind: "feedback" })
  .orThrow();

const post = await posts
  .create(ideas.id, {
    title: "Dark mode for reports",
    body: "The exported PDFs ignore the theme.",
  })
  .orThrow();
```

`saveBoard` creates a board, or with `id` changes it; the key can't change
once set and `archived: true` hides it from `boards()`. `create` writes a
post that shows at once, or with the `moderation: "pre"` option waits for a
moderator. A banned member can't post. `update(postId, input)` changes the
caller's own post while it isn't locked, or any post for a moderator, and
`delete(postId)` removes a post with its votes. `postInputSchema` and
`postBoardInputSchema` validate the inputs.

`list({ boardId, status, authorId, sort })` returns the posts the caller
reads. `sort` is `active` (pinned first, then the latest activity, the
default), `new` or `top` (most votes). Posts merged into others stay out
unless you pass `merged: true`. Each post carries its `votes` tally with the
caller's own vote, its `replyCount` and whether the caller wrote it
(`mine`).

## Votes and replies [#votes-and-replies]

```ts
const tally = await posts.vote(post.id, 1).orThrow();
// { score: 4, up: 4, down: 0, vote: 1 }
```

`vote(postId, value)` sets the caller's vote: `1` up, `-1` down, `0` clears
it. Replies are comments on the subject `post`, so the comments block's
client and recipes thread them:

```ts
await comments
  .create({ type: "post", id: post.id }, { body: "Agreed" })
  .orThrow();
```

A reply on a locked, merged or hidden post fails unless a moderator writes
it.

## Moderate [#moderate]

```ts
await posts
  .setState(post.id, { status: "planned", pinned: true, locked: false })
  .orThrow();

const kept = await posts.merge(duplicate.id, post.id).orThrow();
```

`setState` sets the status (`open`, `under_review`, `planned`,
`in_progress`, `done` or `closed`), the pin and the lock. `merge` closes a
duplicate, points it at the post it kept and moves its votes there, unless a
voter already voted on that post; replies stay on the duplicate.

Every post has a moderation item. A report through the moderation block
queues it, and hiding or removing it there sets the post's `moderation` to
`hidden` or `removed`, which keeps it out of members' lists and stops votes
and replies. Under `moderation: "pre"`, a new post and an author's edit
start as `pending` until a moderator approves them.

## Extra columns [#extra-columns]

`options.extraColumns` adds columns to the posts table, such as a category or
an effort estimate. `create` and `update` write them from `fields`, and
every post carries them in `fields`:

```ts title="better-supabase.config.ts"
blocks: {
  posts: { options: { extraColumns: { category: "text", effort: "integer" } } },
},
```

Pass a Standard Schema as `fields` to `createPosts` to type them: writes
validate them before the database and reads parse them.

## Options [#options]

```ts title="better-supabase.config.ts"
blocks: {
  posts: { options: { moderation: "pre" } },
},
```

| Option          | Default  | What it does                                                           |
| --------------- | -------- | ---------------------------------------------------------------------- |
| `moderation`    | `"post"` | `post` shows posts at once; `pre` holds new posts and edits for review |
| `maxBodyLength` | `20000`  | the longest post body, in characters                                   |
| `extraColumns`  | none     | extra columns on the posts table                                       |

## Events [#events]

The block writes `post_board.saved`, and per post `post.created`,
`post.updated`, `post.status_changed`, `post.moderated`, `post.merged` and
`post.deleted`, to the [outbox](/docs/blocks/outbox). See
[Events](/docs/extending/events).

| Error code             | When                                                             |
| ---------------------- | ---------------------------------------------------------------- |
| `POST_FORBIDDEN`       | the caller lacks the permission, is banned or the post is locked |
| `POST_INVALID`         | a title, status, vote or body doesn't validate                   |
| `POST_NOT_FOUND`       | there is no such post, or the caller can't read it               |
| `POST_BOARD_NOT_FOUND` | there is no such board in the organization                       |