Cariosan

Changelog

Notable changes shipped in each Cariosan release.

Updated Apr 2026

Cariosan follows semantic versioning. Pre-1.0 minor versions may include breaking changes; patch versions never do. This page is manually curated from release tags; published server images are listed on GHCR (ghcr.io/cariosan/server).

Unreleased

Post-MVP — channel avatars + group roles (RBAC, hybrid model): group channels gain an optional avatar_url (icon) and per-member roles (admin | member). On create, pass created_by (a user's external_id) — that user is added and made admin; everyone else is a member (direct channels have neither). Two management paths: the backend/API-key path stays god-mode (PATCH /v1/channels/:id to rename/set-avatar), while a new client/JWT path lets an admin manage the group from the appPATCH /v1/me/channels/:id (name/avatar), POST/DELETE /v1/me/channels/:id/members, and PATCH /v1/me/channels/:id/members/:user (promote/demote). Non-admin members get 403; the last admin can't be demoted. GET /v1/channels/:id/presence now returns each member's role. @cariosan/client: Channel.avatarUrl + channel.updateChannel({name,avatarUrl}) / addMembers / removeMember / setMemberRole(extId, role); the mobile example renders the group avatar in its header.

Changed — reactions are now one-per-user-per-message (WhatsApp-style): a user can have at most one reaction on a given message. Reacting with a different emoji replaces the previous one (the server fans out a reaction.remove for the old emoji followed by a reaction.add for the new, so every client's counts stay correct); reacting with the same emoji again is a no-op. Previously a user could stack multiple distinct emoji on one message. This is enforced server-side in a single transaction, so it holds across the REST API, the WebSocket gateway, and all SDKs without any client changes. MessageList (@cariosan/react-native) now also passes currentUserId through to useMessages so the "reacted by me" highlight updates live.

Fixed — @cariosan/react/@cariosan/react-native now re-sync on reconnect: useMessages (and therefore MessageList) re-fetches the latest page on every WebSocket (re)connect and merges anything it missed. The WS doesn't replay frames sent while a client was disconnected — most commonly a mobile app that backgrounded (the SDK closes the socket) and then foregrounded — so a message sent during that gap used to never appear until a full reload, and the sender's ticks stayed grey (the recipient's read marker never advanced because the on-screen message list never changed). Live message.new is now also de-duplicated against the re-synced page. No API changes.

Fixed — live delivery for channels created/joined mid-connection: a WebSocket connection used to subscribe only to the channels you belonged to at connect time, so a channel created (or an add-members) while you were already connected delivered no live events — new messages and read/delivery receipts didn't appear until you reconnected. Now, when a channel is created or members are added, any affected member who is currently connected (on any server in the fleet) is subscribed to the channel's live topic immediately — no reconnect required. The signal rides each member's own topic over Redis pub/sub, so it works across a multi-server deployment. No API or SDK changes.

@cariosan/react-nativeMessageList now marks the channel delivered + read while it's on screen (new markReadOnView prop, default true), so the other side's ✓✓ / blue ticks actually advance. Previously delivery/read were only marked on a live incoming message.new, so receipts never progressed for backlog messages or when the recipient opened the chat later.

@cariosan/clientChannel now exposes name (alongside type), refreshed from listChannels(). A UI can title a group by its channel name + member count and a direct chat by the other participant — the mobile example's header branches on channel.type (and shows sender names only in groups).

@cariosan/react-native attachments + emoji: MessageInput gains a 🙂 emoji inserter (tap to drop an emoji into the draft) and an optional 📎 attach button (onAttachPress — the host owns the picker + upload). MessageList now renders image attachments as thumbnails and other files as a tappable document card. The mobile example wires an attachment panel (Document / Camera / Gallery) on top of expo-image-picker + expo-document-picker. Attachments now accept documents (PDF, text, CSV, the Office formats, zip, + a generic fallback) alongside images, and a captionless photo overlays its time + ticks on the image (WhatsApp-style). Attachments stage a preview first and upload on send (MessageInput's attachmentPreview + onSendAttachment), with an upload progress % (uploadAttachment(file, meta, onProgress) reports 0..1 via XHR; the example renders a progress bar). Emoji-only messages render large + bubble-less (jumbomoji). SDK: channel.uploadAttachment(file, { filename, mimeType }) accepts explicit metadata for React Native (where a local-file Blob lacks a name), and sendMessage now routes attachments through the image message type so an empty caption is allowed.

@cariosan/react-native UI polish: MessageList now renders a familiar chat layout — day separators (Today / Yesterday / date), left/right bubbles with bottom-right time + read-receipt ticks, and MessageInput gains a rounded composer pill + circular send button (all in the Cariosan brand palette). New showSenderNames prop (default true) hides the sender name on received bubbles for 1:1 chats. New MessageSelectionProvider + useMessageSelection enable hold-to-select: long-pressing a message highlights it and floats a quick-reactions row above it, while your header reads the selection to show a contextual action bar (reply / copy / delete).

Fixed — realtime delivery for REST-sent messages: messages created via the REST API (POST /v1/channels/:id/messages, and therefore every @cariosan/client sendMessage) now broadcast message.new to the channel's WebSocket subscribers — including the sender's own devices — exactly like messages sent over the WS protocol. Previously only WS-native sends fanned out, so a message sent through the SDK was saved but never appeared live until a history refetch. The fan-out is now a single point inside the message service, shared by both send paths.

Post-MVP — Phase 21b (blocks & mutes): Block — an end-user safety control: client.blockUser(externalId) hides a user's messages from your history + search (workspace-wide), unblockUser reverses it, listBlocked() returns the list (filter the live WS stream client-side with it). Mute — a moderation/admin posting-mute: a muted user's sends are rejected (403 MUTED) until expiry; written automatically by the auto_mute moderation action. New internal/relations package (consumer-side message.RelationGuard — message stays decoupled), blocks + channel_mutes tables, and POST/DELETE/GET /v1/me/blocks. @cariosan/client adds blockUser/unblockUser/listBlocked.

Post-MVP — Phase 20 (push notifications): FCM push for new messages, opt-in + privacy-first. Clients register device tokens via client.registerDevice(token, platform) (POST /v1/me/devices); on a new message the server pushes a minimal notification (title only — "{sender} sent a message" + channel_id/message_id in data, never the body) to every other channel member's devices. FCM-only (Android + iOS-via-APNs + Web through one API), dispatched via gopackx/go-notification's FCM driver. BYO Firebase for self-host (CARIOS_FCM_SERVICE_ACCOUNT_FILE); push is off (registration still works, nothing delivered) until configured — Cariosan charges nothing. The push core is decoupled (a Sender interface; the FCM adapter is isolated) and the message service stays push-free via a Notifier hook. @cariosan/client adds registerDevice/unregisterDevice. Verified by unit + testcontainers tests with a mocked sender; real FCM delivery needs your Firebase project + device.

Post-MVP — Phase 19 (React Native SDK): a new @cariosan/react-native mobile UI kit for iOS + Android. The headless core is reused verbatim — @cariosan/client runs in RN unchanged, and the framework-agnostic hooks from @cariosan/react (all DOM-free) are re-exported — so engagement features (quoted reply, @-mentions, read receipts, search) behave identically to web. Native components: MessageList (FlatList with mention highlighting, quote previews, reactions, receipt ticks, Reply action), MessageInput (@-autocomplete + reply chip), ChannelList (unread + mention badges), TypingIndicator, PresenceBadge. The mobile-react-native example is rebuilt on the kit. Verified by TypeScript against real RN types + headless component tests (react-test-renderer); native device rendering is the integrator's check.

Post-MVP — Phase 21a (automatic moderation): in-core, non-AI content moderation, off by default (opt-in per workspace). Every send is evaluated before it persists; a matched rule applies the workspace's action — flag (deliver + record), mask (replace matched spans with ***), or reject (422 MESSAGE_REJECTED, no row). Rules: profanity (built-in Indonesian + English wordlist, whole-word, + custom blocklist), link (URLs), phone (contact-number leakage), flood (repeated messages). Every action is written to a moderation_actions audit trail and fans out a new message.moderated webhook. Settings live in a per-workspace moderation_settings table (Cloud dashboard wiring comes later). The message service stays decoupled from the moderation package (a Moderator interface + a cmd adapter). AI toxicity classification remains a separate, optional, later add-on.

All 16 MVP implementation phases have shipped:

  • Workspace + API key auth (Phase 2)
  • User management with JWT issuance (Phase 3)
  • Channels: direct + group with cascade delete (Phase 4)
  • Messaging REST: send, history, read markers (Phase 5)
  • WebSocket gateway with Redis pub/sub fanout (Phase 6)
  • Presence + typing (Phase 7)
  • Attachments via presigned S3 URLs (Phase 8)
  • Webhooks with retry + auto-disable (Phase 9)
  • TypeScript client SDK (Phase 10)
  • React components and hooks (Phase 11)
  • Go server SDK (Phase 12)
  • Production Docker image + Compose stack (Phase 13)
  • Documentation site (Phase 14)
  • Example applications (Phase 15)
  • Observability (Phase 16)

Post-MVP — Phase 18a (engagement): message reactions (add/remove, per-emoji aggregation with reacted_by_me), editing (author-only, edited_at), and unsend (author-only soft-delete tombstone, content blanked). Available across the REST API, the WebSocket gateway, @cariosan/client (addReaction/removeReaction/editMessage/deleteMessage + reaction.add/reaction.remove/message.updated/message.deleted events), and @cariosan/react (useMessageActions + live useMessages). New webhook events: message.updated, message.deleted, message.reaction.added, message.reaction.removed. The WebSocket envelope gains an optional v version field (forward-compatible: clients ignore unknown event types + fields).

Post-MVP — Phase 18c (quoted replies): send a message as a reply by setting parent_id to the message you're quoting. The server validates the parent lives in the same channel and embeds a truncated quoted_message preview (author, content, type, and a deleted flag for unsent parents) on the returned message, on history, and on the live message.new event. Available across the REST API (parent_id on send), the WebSocket gateway (parent_id on message.send), @cariosan/client (sendMessage({ text, parentId }) + Message.parent_id/Message.quoted_message), and @cariosan/react (MessageList renders the quote above the reply; wrap a room in the new ReplyDraftProvider for the built-in compose flow — per-bubble Reply button, quote chip, and tap-to-scroll — or drive it yourself with useReplyDraft()). No new webhook events — replies arrive as normal message.created payloads carrying parent_id and quoted_message.

Post-MVP — Phase 18b (read & delivery receipts): WhatsApp-style ticks, cursor-based (one last_delivered_at + last_read_at per member — no per-message rows). A message is ✓ sent, ✓✓ delivered when every other member's delivery cursor passed it, ✓✓ blue read when every other member's read cursor passed it. New inbound WS event channel.delivered (the SDK auto-acks on every inbound message); channel.read / channel.delivered broadcast as origin-filtered receipts. New REST GET /v1/channels/:id/receipts. Workspace privacy toggle read_receipts_enabled (default true) gates blue-tick broadcasts (delivery still flows). @cariosan/client adds getReceipts(), receiptState(message), markDelivered(), and a receipt.update event; @cariosan/react adds currentUserId ticks on MessageList + a useReadReceipts() hook.

Post-MVP — Phase 18e (message search): relevance-ranked full-text search over message content, backed by a generated tsvector column (Postgres simple config — language-agnostic, no stemming, sane for a mixed Indonesian + English corpus) + GIN index. New REST GET /v1/channels/:id/messages/search (one channel) and GET /v1/search/messages (across the caller's channels, member-scoped — never leaks non-member channels). Supports websearch syntax ("quoted", OR, -exclude), excludes deleted messages, and paginates like history. @cariosan/client adds channel.searchMessages(q, opts) + client.searchMessages(q, opts); @cariosan/react adds a useSearch(channelId?) hook (box styling left to consumers). This completes the Phase 18 engagement wave (reactions/edit/unsend, quoted reply, mentions, receipts, search).

Post-MVP — Phase 18d (mentions): @-mention channel members by passing their external_ids in mentions on send — explicit ids, no server-side text parsing. The server validates each is a member (a non-member rejects the send), resolves them to a mentions array ([{ user_id, external_id, name }]) on the message, and pings each mentioned member on their own WebSocket topic via notification.mention (delivered even when they're not viewing the channel). GET /v1/me/channels gains mention_unread_count per channel. New webhook event message.mentioned (and message.created now carries mentions). @cariosan/client adds sendMessage({ mentions }), Message.mentions, and a mention event on both the Cariosan instance and the Channel. @cariosan/react adds an @ autocomplete in MessageInput, mention-token highlighting in MessageList, and useMentions() + a ChannelList mention badge.

Currently in pre-launch: code-quality hardening and staging validation with design partners. Pre-1.0 minor versions may still include breaking changes until v1.0 freezes the public API and Docker image surface. Watch this page for updates.

How releases are cut

Release tags v*.*.* trigger a multi-arch image build (amd64 + arm64) pushed to GHCR under both latest and the semver tag. See operations for the upgrade workflow.

Was this page helpful?

On this page