Hinatadocs

Architecture

Hinata has two parts: a Flutter client and a Spring Boot server. They talk over a versioned REST API, backed by MongoDB and S3-compatible storage.

The components

From client to storage:

  • App (Flutter): one codebase, six targets: Android, iOS, the web, macOS, Windows and Linux (a native GTK 3 desktop application, application id com.ahmadre.hinata). State with bloc/cubit, routing with go_router, localization with i18next (English + German), networking through dio inside an ApiClient. Charts are drawn with fl_chart.
  • Server (Spring Boot 4, Java 21): exposes the REST API under /api/v1, holds all business logic and authorization, and streams live updates.
  • MongoDB: the system of record. Production runs a replica set (2 data nodes + 1 arbiter) with TLS and X.509 client authentication.
  • S3 / MinIO: object storage for attachments and avatars, with randomized object keys and presigned downloads.
  • SMTP: outbound mail (verification, notifications, password reset). Mailpit stands in during development.
  • Hinata Connect gateway: a central relay for push and universal links, so the single published app can serve many self-hosted servers.

The app → server path

Every network call from the app goes through a single ApiClient built on dio. It handles tokens and headers so individual screens don't have to:

  • It attaches the current Bearer access token to authenticated requests.
  • It sends an Accept-Language header (en or de) so the server can localize error messages.
  • On a 401, it calls the refresh endpoint, swaps in a fresh access token and retries the original request once. If refresh fails, it clears the session and routes to login.

The server exposes a stable, versioned surface at /api/v1. The full endpoint list is in the API reference.

Multi-server by design

The native app has no built-in server URL. Users save one or more servers and switch between them, and access tokens are scoped per server. The web build may default to its own origin. That is why the same published app works against any Hinata server. See Branding & custom clients.

Live updates with SSE

The server pushes changes to connected clients over Server-Sent Events (SSE), so clients don't poll. Attachments are the clearest example: when a file is added to or removed from an issue, every open client streaming that issue gets the change immediately at:

GET /api/v1/issues/{issueId}/attachments/stream

SSE is a one-way, long-lived HTTP stream. It is cheap, works well through proxies and needs no WebSocket upgrade. Make sure your reverse proxy does not buffer these responses.

Request lifecycle & token refresh

A typical authenticated request:

  1. App: sends a request through ApiClient. dio attaches the Bearer access token and Accept-Language.
  2. Server: authenticates the JWT (HS512), enforces per-IP rate limits and checks authorization (for example, /api/v1/admin/** requires the ADMIN role).
  3. Controller → Service → Repository: the service layer applies business rules and reads or writes MongoDB. Attachment bytes go to S3/MinIO.
  4. Response: a stable, localized JSON body. Either the success payload or an error resolved from messages.properties in the client's language, without stack traces.

Access tokens are short-lived. Refresh tokens live longer but are rejected for API access and can only mint new access tokens. When an access token expires mid-session, the user never sees the refresh:

App ──GET /issues (expired access token)──▶ Server
App ◀──────────── 401 Unauthorized ────────── Server
App ──POST /auth/refresh (refresh token)──▶ Server
App ◀──────── new access token ───────────── Server
App ──GET /issues (retried, new token)────▶ Server
App ◀──────────────── 200 OK ─────────────── Server

See Authentication for the full token model.

Runtime settings in MongoDB

Most operational configuration is stored in MongoDB and managed from the app's Admin area. It is not fixed in environment variables at boot. This covers SSO providers, e-mail ingest (IMAP), push, Git OAuth apps and app settings like the minimum client version.

Two rules follow:

  • The database overrides the environment. Environment variables such as hinata.app.* are defaults. A value set in the Admin area wins.
  • Changes apply without a restart. Update an SSO provider or a feature flag and it takes effect on the next request, with no redeploy or container restart.

Secrets are write-only

In the admin API, secrets (OAuth client secrets, tokens, passwords) are write-only. You can set them, but they are never returned. Stored Git access tokens are also AES-GCM encrypted at rest.

So bootstrapping only needs a handful of environment variables (see Configuration reference). Everything else is configured once the server is up.

Localized errors

Error messages are resolved server-side from resource bundles: messages.properties (English, the default) and messages_de.properties (German), keyed by the client's Accept-Language header. The server returns a stable, machine-readable error whose message is already in the right language. The client does no translation.

The Connect gateway

Push notifications and universal links run through the Hinata Connect gateway, a hosted service run by the app publisher. They are not built into each server.

  • Your server connects to the gateway. The app's push credentials live there, so self-hosters need no Firebase project of their own.
  • Universal links open the app on the correct server, wherever the invite or reset link came from.
  • The app publisher operates and secures the shared gateway. Self-hosters don't run or manage it. Override it with HINATA_GATEWAY_BASE_URL only when you ship your own branded app.

Linux gets the notifications, not the push

Push needs a delivery service from the operating system, and Linux has none to register with. firebase_messaging has no Linux implementation. A Linux client therefore never registers a push token, and the gateway has nothing to deliver to. The same events still reach the user in the app's notification centre and by e-mail. The push switch in account settings stays active, because that preference belongs to the account and still governs the same person's phone.

This lets the one published app serve any number of independent, self-hosted Hinata servers. See Hinata Connect gateway.

Where to go next