Social guides
Guides

Hashtags.

Write #storymode in a post body and Social indexes it. The directory is per-community, the parser lowercases + dedupes + caps at 30 per body. Indexed inside the same DB transaction as the post insert — a failed post never leaves orphan tag rows.


1

Use #tags in a post

Just write #whatever in body. The parser pulls every #[a-z0-9_]{1,32} match (case-insensitive, lowercased on store), dedupes, and caps at 30 per body. Anything beyond 30 is silently dropped, and a tag longer than 32 characters matches nothing at all — it is never truncated into a junk tag.

bash
curl -X POST \
  -H "Authorization: Bearer pcft_live_..." \
  -H "Content-Type: application/json" \
  -d '{"actor_id":"<actorId>","body":"big day! #storymode #v2 launch is live"}' \
  "https://social.productcraft.co/v1/communities/<communityId>/posts"

2

Browse the directory

Per-community, ordered by first_used_at desc — newest tags first. Cursor-paginated.

bash
# newest tags (default)
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<c>/hashtags?limit=20"

# trending — most posts in a window. window_post_count is populated
# only in this mode. Results are cached for 5 minutes.
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<c>/hashtags?order=trending&window=24h"

order=trending ranks tags by their published-post count inside window=24h or 7d (counted through post.created_at). The literal tag #trending is unaffected — it stays reachable at /hashtags/trending/posts.


3

Posts for a tag

order=chronological (default) pages by post created_at desc; order=ranked reuses the discover recency+engagement scoring and pages on a (score, id) cursor. Both return only published posts — drafts, removed, hidden, expired, and private-author posts are filtered out. An unknown tag is a 404.

bash
# ranked — best posts for the tag, not just newest
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<c>/hashtags/storymode/posts?order=ranked&limit=20"

4

Prefix autocomplete

Backed by a text_pattern_ops index on (community_id, tag) — fast even on millions of tags. q is required and case-insensitive (the server lowercases it for you); limit defaults to 20, max 50. The envelope is the standard one, but pagination is always null — autocomplete returns a single bounded page with no cursor.

bash
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<communityId>/search/hashtags?q=sto&limit=10"

5

Tag detail with post count

Returns the directory row plus a live count of currently- published posts using the tag.

bash
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<communityId>/hashtags/storymode"
json
{
  "community_id": "...",
  "tag": "storymode",
  "first_used_at": "2026-04-01T12:00:00.000Z",
  "window_post_count": null,
  "post_count": 17
}

Internals

Why the transaction matters

Hashtag rows are inserted INSIDE the same DB transaction as the post insert. The naive approach — insert the post, then insert the hashtags — produces orphan directory entries when the post insert fails: the hashtag row sits in the directory pointing at a post that never came into existence.

The TX-aware path also covers updates: editing a post body re-runs the parser, replaces the post'spost_hashtag rows, and ensures any new tags appear in the directory — all in one TX. If the update rolls back, the index doesn't drift.


Permissions

PAK scopes

  • List + posts-for-tag + autocomplete: social.list on pcft:agora:community/<communityId>.
  • Tag detail: social.read on the same URN.
  • Writes ride the post-create / post-update endpoints and use those scopes; no extra grant needed for the hashtag side-effect.