steady-chat is a small, self-hosted real-time chat application delivered as an Android app and a web version. It is built for a technical operator audience: people who run their own infrastructure, sign in through their own Authentik identity server, and keep a fast, legible chat tool open all day.
The product intent is narrow and deliberate. A user signs in through Authentik, the app shows their previous chats, they can create a new chat, and inside a conversation they exchange messages while seeing when the other participant is typing. User records live only in MongoDB. New users are provisioned manually in Authentik by the operator; the first successful sign-in creates that user's MongoDB record.
The audience is the signed-in chat participant — a single in-product human role, the Chat User — plus the operator who manually provisions Authentik accounts outside the product. steady-chat is not an enterprise dashboard, not a consumer social network, and not a self-service signup product.
steady-chat is a cross-platform chat client with a shared backend.
Delivery ownership. steady-chat owns its own first-party UI on both Android and web. The five surfaces — Landing, Login, Chats, New Chat, Conversation — are application-owned custom pages. Landing is anonymously reachable and explains the product before identity is established. Login is the anonymous entry boundary that hands off to Authentik; it does not itself own credential verification. Chats, New Chat, and Conversation require a signed-in session.
Identity ownership. Identity is provider-owned. Authentik verifies credentials and issues the session; steady-chat never stores or checks passwords. Because the accepted workflow requires that a user's chats and messages remain bound to the correct participant across sessions, steady-chat maintains an application-side user record in MongoDB, created on first successful sign-in if it does not already exist. This is continuity of the accepted chat lifecycle, not a second account system: there is no in-app registration, no password reset, no profile editing, and no account deletion.
Provisioning boundary. Adding a new user to Authentik is an operator action performed outside the product, in Authentik itself. steady-chat does not expose user administration, invitations, or enrollment.
Current vs. future. Everything described in this document is current. No future-horizon features are accepted; anything not listed here (group chats, attachments, message editing, notifications, search across messages, presence beyond typing) is out of scope for this generation.
Not applicable — no reference directive in this project declares content_source.
steady-chat set in Silkscreen; a single sentence of IBM Plex Sans body text describing the app; a non-interactive mock of the app showing two message slips and a blinking three-square typing indicator; a pixel speech-bubble glyph bleeding off the right edge; a dotted-grid texture band across the full width.FR-1 — Cross-platform chat client As a Chat User, I should be able to use steady-chat on Android and on the web, so that I can chat from either device. (explicit)
FR-2 — Sign in through Authentik As a Chat User, I should sign in through Authentik, so that my identity is verified by the operator's own authentication server. (explicit)
FR-3 — Manual Authentik provisioning prerequisite As a Chat User, I should only be able to sign in if an account for me has already been created manually in Authentik, so that access stays under operator control. (required_inference)
FR-4 — MongoDB user record created on first login As a Chat User, I should have my user record created in MongoDB on my first successful sign-in if it does not already exist, so that my chats stay bound to me. (required_inference)
FR-5 — Users are available only from MongoDB As a Chat User, I should see only users that exist in MongoDB, so that the participant list reflects the operator's provisioned set. (explicit)
FR-6 — Show previous chats after sign-in As a Chat User, I should see my previous chats after signing in, so that I can resume a conversation. (explicit)
FR-7 — Create a new chat As a Chat User, I should create a new chat, so that I can start a conversation with another user. (explicit)
FR-8 — Send and receive messages in a conversation As a Chat User, I should exchange messages inside an open chat, so that the conversation is interactive. (explicit)
FR-9 — See when the other user is typing As a Chat User, I should see when the other user is typing, so that the conversation feels live. (explicit)
FR-10 — Resume a previous conversation As a Chat User, I should open a previous chat from my list, so that I can continue where I left off. (explicit)
FR-11 — Sign out As a Chat User, I should sign out, so that I can end my session on a shared device. (required_inference)
Product context. The Chat User is the only in-product human role. They are a technical operator-adjacent participant in a small self-hosted deployment: their account was created for them manually in Authentik by whoever runs the instance, and they sign in with those credentials rather than registering. They use steady-chat on Android and on the web, often keeping it open all day as a working tool rather than a destination they visit occasionally.
Primary goal. Hold a fast, legible, real-time conversation with another provisioned user, and pick up prior conversations exactly where they left off.
Distinct accepted responsibilities.
Relevant inputs and decisions. Their Authentik credentials; which conversation to open; which participant to start a new chat with; what to type and when to send; whether to keep a conversation open or return to the list.
Interactions with other accepted participants. The Chat User's counterpart is another Chat User — the other participant in the conversation. Their typing is what produces the indicator the first user sees, and their messages are what arrive in the thread. The operator who provisions Authentik accounts is not an in-product participant; that work happens outside steady-chat.
Observable success. A working real-time chat session: the user signs in, sees their previous chats, opens or creates a conversation, sends and receives messages, and sees the typing indicator appear and clear at the right moment — with the same behavior on Android and on the web.
Source-backed constraints on this role. The role exists only for identities provisioned in Authentik; there is no self-service signup. The role's user record is created in MongoDB on first successful sign-in. The role sees only users that exist in MongoDB.
Muse and headline. Susan Kare — charming clarity: pixel-perfect warmth for a small, real-time chat app. Tiny legible symbols with personality, a grid you can feel, monochrome plus a small crisp accent palette on a light ground. Trust comes from legibility and feedback, not from gloss.
Palette (light mode).
| Role | Hex | Use |
|---|---|---|
| Background | #F4F1E8 | Warm paper ground carrying everything |
| Surface | #FFFFFF | Raised message surfaces and the composer only |
| Text | #1B1B1F | All body and heading text (~15:1 on the paper ground) |
| Primary | #1F6F5C | Own messages, primary buttons, active nav rail — deliberately not blue |
| Accent | #E4572E | Live events only: typing indicator, unread counts, send affordance |
| Muted | #8A8578 | Timestamps, metadata, secondary labels |
No gradients anywhere. Colour is flat and countable, like a 1-bit-plus palette.
Typography.
clamp(40px, 7vw, 96px) for the landing wordmark, Silkscreen.Shape language. Chunky 2px ink outlines and small 4–6px radii, never larger — corners stay almost square so the pixel grid reads. Avatars are square tiles with a 1px inner rule and Silkscreen initials. Buttons are rectangular with a 2px bottom edge that looks like a physical key cap and depresses on press. Dividers are 1px dotted ink at 20% opacity, like a ruled notebook. No blobs; no pills except the typing indicator's three-square pulse.
Layout. A fixed three-column desk at 1280px: a 72px icon rail on the left (chat, new chat, search, profile — each a Kare-style glyph with a label revealed on hover), a 320px conversation list in the middle on the paper ground with dotted rules between rows, and the message canvas on the right in white with the composer pinned to the bottom. At 768px the rail collapses into a top bar and the list and canvas swap as full-width views. At 375px it is a single column: sticky header with the wordmark in Silkscreen, a horizontally scrollable row of conversation chips, then the thread. Message bubbles are left-aligned slips with a 12px indent for the other party and a pine-green left border for your own; timestamps sit on their own dotted rule beneath each cluster.
Imagery. The product is the imagery. Hand-built 16×16 and 24×24 pixel glyphs for every action — a speech bubble with a tail, a plus in a square, a magnifier, a bell, a paperclip, a checkmark pair for read receipts. Avatar placeholders are generated 5×5 pixel identicons in the pine/orange/ink palette. The landing page uses one large pixel illustration of two speech bubbles overlapping, drawn on a 16px grid, plus a dotted-grid texture band. No photography, no 3D, no stock people, no gradient blobs.
Readable text and controls stay whole at 375px, 768px, and 1280px: headlines, wordmarks, labels, numbers, and controls stay entirely inside the viewport and their container, wrapping or scaling to fit, with no other element covering them. Decoration and imagery may bleed, crop, or overlap as the direction asks, provided they cover no readable text or control.
The desk. The public entry is a full-width paper-ground band split asymmetrically 7/5.
On the left, the wordmark steady-chat is set in Silkscreen at clamp(40px, 7vw, 96px), stacked over three lines, with a pine-green 2px rule beneath it and a single sentence of IBM Plex Sans body text under that.
On the right, a live, non-interactive mock of the app itself: a white message canvas holding two slips — one white with a 4px pine-green left border, one paper-ground indented 12px — a blinking three-square typing indicator in signal orange sitting exactly where the next message will appear, and a pixel speech-bubble glyph at 120px bleeding 24px off the right edge of the viewport.
A dotted-grid band runs the full width beneath both halves.
The concept is implementable with the accepted content alone: the wordmark, one sentence of product description, the sign-in entry point, and a presentational mock of the conversation surface. The mock is explicitly non-interactive and introduces no behavior. There is no centred headline, no blue button, and no gradient.
Interaction Model: Static Motion Tempo: restrained Hero Dimensionality: flat
Landing Hero Motion Brief.
NFR-1 — Real-time delivery. Messages and typing status must propagate between participants over a persistent channel with low enough latency that the typing indicator reads as live. (explicit — "interactive user can see if other user typing")
NFR-2 — Durable conversation state. Conversations, messages, and user records must persist in MongoDB so that previous chats survive sign-out, app restart, and device change. (explicit — "show previous chats"; planning scope durable_state_required: true)
NFR-3 — Ephemeral presence state. Short-lived typing and presence state must be held in Redis rather than written to MongoDB, so that transient signals do not pollute durable conversation history. (planning scope redis_required: true)
NFR-4 — Provider-owned authentication. Credential verification must be performed by Authentik. steady-chat must not store, transmit, or validate passwords itself. (explicit — "Server for authentication will be Authentik")
NFR-5 — No self-service enrollment. The product must not expose registration, invitation, or user-administration surfaces. Accounts are created manually in Authentik by the operator. (explicit — "New user we will add manually to Authentik")
NFR-6 — Cross-platform parity. The Android app and the web version must present the same five surfaces and the same chat behavior. (explicit — "work on Android and also will have a web version")
NFR-7 — Legibility and feedback. Status must be readable at a glance: the typing indicator, unread counts, and message ownership must be distinguishable without relying on colour alone, and all readable text and controls must remain whole at 375px, 768px, and 1280px. (creative direction)
NFR-8 — Reduced motion. With prefers-reduced-motion, the interface must present a usable static arrangement: the typing indicator holds a static three-square state and presence dots hold steady. (creative direction)
Constraints (binding).
Assumptions (narrow, labeled).

steady-chat is a small self-hosted chat for people who run their own infrastructure — sign in, pick up your previous conversations, start a new chat, and watch live typing as it happens.
Sign in
steady-chat is a small self-hosted chat for people who run their own infrastructure — sign in, pick up your previous conversations, start a new chat, and watch live typing as it happens.
Sign in
No comments yet. Be the first!