No description
Find a file
2026-09-22 12:27:10 -07:00
.agents/skills Upgrade to welshman 0.11.0 2026-09-21 17:59:11 +00:00
.gitea/workflows Add mirror workflow 2026-09-15 13:51:38 -07:00
.husky Update link_deps for pnpm 11 2026-08-13 14:35:51 -07:00
android Add native app shortcuts for common features (#604) 2026-09-21 19:51:15 +00:00
assets Update app icon 2025-01-17 09:08:55 -08:00
docs Consolidate release scripts 2026-09-21 10:53:27 -07:00
e2e Add desktop auto-updates and Linux icon integration (#621) 2026-09-22 12:27:10 -07:00
electron Add desktop auto-updates and Linux icon integration (#621) 2026-09-22 12:27:10 -07:00
fdroid Add native app shortcuts for common features (#604) 2026-09-21 19:51:15 +00:00
ios Add native app shortcuts for common features (#604) 2026-09-21 19:51:15 +00:00
packages Protect desktop secrets with OS-backed storage (#561) 2026-09-22 16:32:08 +00:00
scripts Require braces on conditionals 2026-09-21 10:53:27 -07:00
src Wrap long words and links so a chat bubble cannot overflow the screen (#622) 2026-09-22 18:51:59 +00:00
static Update universal links 2026-04-16 13:50:13 -07:00
.ackrc Support unban/unallow 2026-03-09 21:12:10 -07:00
.dockerignore Fix dockerfile again 2026-05-11 13:20:47 -07:00
.env Tweak landing page when there's an invite code 2026-09-21 10:53:27 -07:00
.fdignore Consolidate release scripts 2026-09-21 10:53:27 -07:00
.gitattributes Add indexer, maybe improve safe area support 2025-05-15 10:15:41 -07:00
.gitignore Added a source-buildable F-Droid Android distribution (#527) 2026-09-18 17:48:04 +00:00
.npmrc Initial commit 2024-08-01 16:49:32 -07:00
.nvmrc Update capacitor, get ios running 2025-01-31 13:14:29 -08:00
.prettierignore Initial commit 2024-08-01 16:49:32 -07:00
.prettierrc Moar upgrades 2026-01-23 10:53:50 -08:00
AGENTS.md Consolidate release scripts 2026-09-21 10:53:27 -07:00
capacitor.config.ts Added cross-platform desktop packaging (#526) 2026-09-14 17:58:14 +00:00
CHANGELOG.md Update changelog 2026-09-15 15:35:52 -07:00
CONTRIBUTING.md Add contributing file 2026-04-02 13:31:37 -07:00
Dockerfile Add a script to bump the version across web, ios, android 2026-08-18 15:22:14 -07:00
eslint.config.js Require braces on conditionals 2026-09-21 10:53:27 -07:00
LICENSE Add license 2024-08-05 14:47:49 -07:00
package.json Protect desktop secrets with OS-backed storage (#561) 2026-09-22 16:32:08 +00:00
playwright.config.ts Cover dictation in the e2e suite (#545) 2026-09-15 17:59:36 +00:00
pnpm-lock.yaml Protect desktop secrets with OS-backed storage (#561) 2026-09-22 16:32:08 +00:00
pnpm-workspace.yaml Add a script to bump the version across web, ios, android 2026-08-18 15:22:14 -07:00
postcss.config.js Upgrade daisyui/tailwind 2026-04-07 15:31:35 -07:00
pwa-assets.config.js Fix env conventions again 2026-04-02 14:01:09 -07:00
README.md Add desktop auto-updates and Linux icon integration (#621) 2026-09-22 12:27:10 -07:00
server.js Update server.js to use new welshman implementation 2026-08-18 15:13:12 -07:00
skills-lock.json Update skills 2026-09-14 14:24:36 -07:00
svelte.config.js Derive the CSP script hashes from app.html 2026-09-15 07:14:24 +00:00
tailwind.config.js Fix agent errors 2026-07-27 16:43:54 -07:00
tsconfig.json Added a source-buildable F-Droid Android distribution (#527) 2026-09-18 17:48:04 +00:00
vite.config.server.ts minimize container size and caching 2026-05-22 15:36:28 -07:00
vite.config.ts Added minimal Capacitor Electron desktop baseline (#424) 2026-09-07 17:42:52 +00:00
zapstore.yaml Update zapstore.yaml 2026-03-09 21:12:51 -07:00

Flotilla

A discord-like nostr client based on the idea of "relays as groups". Supports NIP 29 groups, chat, DMs, threads, calendars, classifieds, zap goals, articles, microblogging, and cross-posting between different contexts.

Install

Hosted spaces are available at flotilla.social.

Features

  • Spaces and rooms, threads, direct and group messages
  • Voice and video calls
  • Calendar events, long-form articles, and polls
  • Reactions, custom emoji, zaps, link previews, and media sharing
  • Invite codes, member management, bans, roles, and reports
  • Push notifications, unread indicators, and per-room mute

If you would like to be interoperable with Flotilla, please check out this guide.

Environment

Create an .env.local file to override any of the values in .env:

Platform branding

  • VITE_PLATFORM_URL - The url where the app will be hosted
  • VITE_PLATFORM_NAME - The name of the app
  • VITE_PLATFORM_LOGO - A logo url for the app. Can be a local path or https link. Must be a PNG file.
  • VITE_PLATFORM_ACCENT - A hex color for the app's accent color (used only for generated manifest, for more control create a custom theme file)
  • VITE_PLATFORM_DESCRIPTION - A description of the app
  • VITE_PLATFORM_ABOUT - URL to your marketing or about page
  • VITE_PLATFORM_TERMS - URL to your terms of service page
  • VITE_PLATFORM_PRIVACY - URL to your privacy policy page
  • VITE_PLATFORM_LOGEE - A hex pubkey which will receive logs users send from their privacy settings
  • VITE_THEME - The visual preset components are styled with: clay, flat, or navy

Platform mode

  • VITE_PLATFORM_RELAYS - A comma-separated list of relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.

Defaults

  • VITE_DEFAULT_PUBKEYS - A comma-separated list of hex pubkeys for bootstrapping web of trust
  • VITE_DEFAULT_SPACES - A comma-separated list of relay urls that new users will be automatically joined to on signup. Each one may optionally include an invite code, delimited by |, e.g. my.space.com|CODE.
  • VITE_DEFAULT_RELAYS - A comma-separated list of relay urls used as default outbox/inbox relays
  • VITE_DEFAULT_MESSAGING_RELAYS - A comma-separated list of relay urls used for encrypted direct messages
  • VITE_DEFAULT_SEARCH_RELAYS - A comma-separated list of relay urls used for search
  • VITE_DEFAULT_BLOSSOM_SERVERS - A comma-separated list of blossom server urls used for file uploads

Infrastructure

  • VITE_INDEXER_RELAYS - A comma-separated list of relay urls used for user profile/key lookup
  • VITE_SIGNER_RELAYS - A comma-separated list of relay urls used for NIP-55 remote signers
  • VITE_BLOCKED_RELAYS - A comma-separated list of relay urls that will be blocked
  • VITE_PUSH_SERVER - URL of the push notification server
  • VITE_PUSH_BRIDGE - WebSocket URL of the push notification relay bridge
  • VITE_POMADE_SIGNERS - A comma-separated list of Pomade signer server URLs (3+ required to enable email signup)
  • VITE_THUMBNAIL_URL - URL of the image thumbnail service

These values won't be used for a built version. Instead, env variables should be provided to scripts/build.sh directly or to the built container.

If you're deploying a custom version of flotilla, be sure to remove the plausible.coracle.social script from app.html. This sends analytics to a server hosted by the developer.

Development

pnpm install
pnpm run dev

See CONTRIBUTING.md for conventions and workflow.

Desktop development (Linux)

The Electron target and its unsigned packages are for development and testing. Release publishing and production signing remain prerequisites for public distribution. Desktop secrets are encrypted using the OS keyring or keychain. If protected storage is unavailable or unreadable, the app warns and keeps secrets only in memory until it closes, leaving the saved file untouched. Unlock or configure the OS keyring and restart to restore persistence. Linux’s insecure basic_text backend is never used.

Use disposable accounts only with unsigned development packages. Production distribution still requires release signing and publishing configuration.

The Electron subproject installs separately, so ordinary web and mobile installs don't download Electron:

pnpm install --frozen-lockfile
npm ci --prefix electron
pnpm run dev:desktop

dev:desktop starts Vite on 127.0.0.1 and runs the Capawesome Electron platform against it, with the Capacitor plugin bridge and frontend HMR. No previous frontend build is needed. It uses the existing Vite port (1847 by default) and fails if that port is occupied. The platform allows one instance at a time, so quit any other desktop instance before switching modes. Restart the command after editing Electron TypeScript, and quit Electron or press Ctrl+C to stop.

To build and run local production assets instead:

pnpm run build:desktop
pnpm run start:desktop

build:desktop builds the frontend without PWA/service-worker registration, synchronizes the Electron platform, and compiles its TypeScript entrypoint. It uses the same branding environment as the web build, and does not synchronize Android or iOS. start:desktop opens the last build without Vite, so rerun build:desktop after frontend changes. The development URL goes only to the desktop run process. Capawesome records it in ignored generated configuration, and production synchronization removes it.

Run pnpm run test:desktop after building to check the Linux desktop window. On a headless Linux runner, use xvfb-run -a pnpm run test:desktop. Such a runner also needs libgtk-3-0t64, since Playwright's chromium dependencies do not cover the GTK libraries Electron links against. The test drops Chromium's sandbox when it runs as root, because Chromium refuses to start that way. This smoke suite does not start a web dev server, and does not run in CI. It verifies nothing about Windows or macOS.

Desktop packaging

pnpm run package:desktop:linux
pnpm run package:desktop:windows
pnpm run package:desktop:macos

Each command rebuilds production assets, copies and updates Capacitor, compiles Electron, vendors its runtime and plugins, and invokes electron-builder without publishing or signing. Outputs land in electron/dist/: a Linux x64 AppImage, a Windows x64 NSIS installer, and separate macOS x64 and arm64 DMGs and ZIPs. The root package version is authoritative, the Capacitor app ID stays stable, and VITE_PLATFORM_NAME supplies the product name. Vite's .env.local overrides apply, and explicit VITE_* environment values take precedence. Use production branding values when building artifacts for others. VITE_PLATFORM_LOGO can be a local or HTTPS image, which packaging resizes to 1024×1024 and stages in ignored output.

Window and Dock icons use the bundled branding image, including during local runs. The default VITE_PLATFORM_LOGO=static/logo.png is the official logo; custom branding remains supported. On Linux, packaged startup registers a hidden desktop entry and a persistent icon under XDG_DATA_HOME (normally ~/.local/share) so Wayland docks can identify the app. Existing user or system launchers are preserved, and development runs never write an entry. The generated entry follows the AppImage path, including immediately after an update renames it. Windows uses the executable icon resources.

Linux packaging requires Linux. Windows packaging from Linux uses the pinned official electronuserland/builder Wine image through Docker, mounting only a temporary copy of the prepared Electron project. Native addons need a target-OS ABI rebuild and cannot use this cross-build path. Native Windows preparation needs Bash on PATH, for example Git Bash. DMG creation requires macOS. On Linux, pnpm run package:desktop:macos --dir prepares unsigned bundles for inspection only, and verifies nothing about the macOS runtime, Gatekeeper, or signing.

To smoke-test a package, run as a non-root user with the sandbox enabled:

FLOTILLA_DESKTOP_EXECUTABLE="/absolute/path/to/application" pnpm run test:desktop

Use the AppImage or installed executable rather than the installer. This checks packaged metadata, local assets, navigation, workers, and CSP using a disposable profile. Installation, reboot, and uninstall still require target-OS testing.

Desktop updates

Packaged apps check once at startup, download available updates, and install them on normal quit. There is no update notification or custom updater UI. Unpackaged development runs do not check. Update errors are logged and leave the app usable. The generic feed is configured in electron/electron-builder.config.mjs; electron-builder generates app-update.yml and the latest*.yml files alongside the installers, including when packaging with --publish never.

Before public desktop auto-updates ship, the release pipeline must guarantee that every stable release selected by Gitea's latest route includes all desktop updater metadata and matching artifacts. A newer mobile-only release would break the desktop feed. Production acceptance also requires platform signing and native Windows/macOS testing. The existing release task does not yet upload updater metadata or macOS ZIPs; that is follow-up work. Until it does, the public feed returns 404 for channel metadata and packaged startup logs an update check failure. This PR provides packaging and runtime support, not an operational public update feed. Do not publish desktop builds to users until the publishing follow-up is complete.

For local updater QA, use disposable copies with temporary A/B versions and an isolated user-data directory. In those copies only, point the builder's generic feed at a local HTTP server. Package both versions and serve B's generated metadata and artifacts. Run AppImage A, wait for B to download, quit normally, and relaunch the installed AppImage to verify its version and saved state. Check that preferences, protected secrets, tray controls, and notification activation survive. Also exercise unavailable/missing files, interrupted downloads, invalid checksums/versions, and an already-current version; failed updates must not install. Never bypass integrity checks. On macOS, build both architectures together and verify the single generated manifest references both ZIPs with matching hashes. Do not hand-create or merge updater manifests.

Releasing

pnpm release takes a tagged commit and ships it everywhere: the web bundle and native projects, the signed APK on gitea and zapstore, the AAB on Google Play, the iOS build on App Store Connect, and the desktop packages. It checks the tag, the changelog section, every credential and every tool up front, and refuses to start if one of them is missing rather than getting halfway. What's left — rolling out on Play, submitting for review — comes back as a list when it finishes.

pnpm bump minor            # or patch, major, or an explicit x.y.z
# write the CHANGELOG.md section for the new version
git commit -am "Bump version"
git tag 1.12.0 && git push origin dev 1.12.0
pnpm release

pnpm release --check runs those checks and reports the plan without building anything. Naming steps runs a subset — pnpm release ios, or pnpm release apk gitea. A step that fails stops the run and prints the command to pick up from there.

step what it does
web scripts/build.sh: web bundle, cap sync, generated icons and splash screens
apk assembleRelease signed with the distribution key, renamed to the path in zapstore.yaml
fdroid reruns F-Droid's own preparation and build against the tag in a throwaway worktree
play bundleRelease signed with the upload key, uploaded to a Play track as a draft
ios cap build ios to an archive and IPA, uploaded with altool
desktop package:desktop:* for this OS
gitea creates the release for the tag from the changelog, attaches the APK and any desktop packages
zapstore zsp publish zapstore.yaml

Release notes come from the CHANGELOG.md section matching package.json's version, so every store shows the same text. The APK and zapstore share one artifact, whose path lives in zapstore.yaml.

F-Droid has nothing to upload — their servers build from the tag themselves — so the fdroid step is a gate instead: it runs their preparation and build against the tag in a throwaway git worktree, and fails the release before anything is published if that build no longer works. Preparation patches source with exact-match replacements, so it breaks quietly when the files it rewrites change. Expect it to take a while; it installs and builds from scratch.

Credentials

These go in .env.local, which is gitignored. pnpm release --check lists whichever are missing along with how to get them.

variable what it is
GITEA_TOKEN gitea access token with write:repository, from Settings → Applications
ANDROID_KEYSTORE_PATH, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEYSTORE_ALIAS the key APKs outside the app stores are signed with; it can never change without breaking updates
PLAY_KEYSTORE_PATH, PLAY_KEYSTORE_PASSWORD, PLAY_KEYSTORE_ALIAS the Play upload key
PLAY_SERVICE_ACCOUNT path to a service account json with the Release manager role, from Play Console → Setup → API access
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH App Store Connect API key with the App Manager role, from Users and Access → Integrations
SIGN_WITH nostr key for zapstore: an nsec, a bunker:// url, or browser

Add _ALIAS_PASSWORD to either keystore prefix when the alias has its own password. PLAY_TRACK (default production) and PLAY_STATUS (default draft) choose where a Play upload lands. Keystores and API keys belong outside the repository; only their paths go in .env.local.

Gradle signs from those variables, so Android Studio still opens and builds the project without them — it just produces an unsigned release build.

Obtainium

Obtainium installs and updates Android apps from their release pages. Gitea and Forgejo share a release API, so it works against this repository:

  • App source URL: https://gitea.coracle.social/coracle/flotilla
  • Override source: Forgejo (Codeberg)

Obtainium reports the git tag as the version.

Deployment

To run your own Flotilla, it's as simple as:

pnpm install
pnpm run build
pnpm run start

Or, if you prefer to use a container:

docker run -d -p 3000:3000 gitea.coracle.social/coracle/flotilla:latest

Alternatively, you can copy the build files into a directory of your choice and serve it yourself:

mkdir ./mount
docker run -v ./mount:/app/mount gitea.coracle.social/coracle/flotilla:latest bash -c 'cp -r build/* mount'

License

MIT