···11+# Release & Distribution
22+33+Build, sign, package, and distribute Lazurite Desktop across macOS, Windows, and Linux. Covers direct distribution (GitHub Releases), Mac App Store, Microsoft Store, content moderation, and auto-update.
44+55+## Distribution Channels
66+77+| Channel | Format | Support Button | NSFW Toggle |
88+| --------------- | -------------------------- | -------------- | ----------- |
99+| GitHub Release | DMG, NSIS `.exe`, AppImage | Yes | In-app |
1010+| Mac App Store | `.app` (sandboxed) | No | Web-only |
1111+| Microsoft Store | MSIX | No | In-app |
1212+| Linux (future) | Flatpak, Snap | Yes | In-app |
1313+1414+The `DISTRIBUTION_CHANNEL` compile-time env var (`github`, `mac_app_store`, `microsoft_store`) gates channel-specific behavior. The frontend reads it via a Tauri command.
1515+1616+## Content Moderation
1717+1818+Port the moderation system from the Flutter app (`lazurite`). The AT Protocol's label system is the backbone - posts, profiles, and media carry labels applied by labelers (official Bluesky labeler + user-subscribed custom labelers).
1919+2020+### Architecture
2121+2222+```text
2323+┌─ Rust Backend ──────────────────────────────┐
2424+│ ModerationService │
2525+│ ├─ Fetch labeler policies (cached in DB) │
2626+│ ├─ Evaluate labels → ModerationDecision │
2727+│ ├─ Send atproto-accept-labelers header │
2828+│ └─ Store preferences per account │
2929+├─────────────────────────────────────────────┤
3030+│ Tauri Commands │
3131+│ ├─ get_moderation_prefs() │
3232+│ ├─ set_adult_content_enabled(bool) │
3333+│ ├─ set_label_preference(labeler, label, │
3434+│ │ visibility) │
3535+│ ├─ subscribe_labeler(did) / unsubscribe │
3636+│ ├─ create_report(subject, reason_type, │
3737+│ │ reason) │
3838+│ └─ moderate_content(subject, labels, │
3939+│ context) → ModerationUI │
4040+├─────────────────────────────────────────────┤
4141+│ SolidJS Frontend │
4242+│ ├─ ModeratedBlurOverlay │
4343+│ ├─ ModeratedAvatar │
4444+│ ├─ ModerationBadgeRow │
4545+│ ├─ ReportDialog │
4646+│ └─ Moderation Settings section │
4747+└─────────────────────────────────────────────┘
4848+```
4949+5050+### Label Evaluation
5151+5252+Each piece of content (post, profile, avatar, media embed) is evaluated against the user's moderation preferences to produce a `ModerationDecision`:
5353+5454+- **Context-aware**: `contentList`, `contentView`, `contentMedia`, `avatar`, `profileList`, `profileView` - different contexts can produce different UI outcomes for the same label.
5555+- **Visibility levels**: `ignore` (no action), `warn` (badge + interstitial), `hide` (blur, removable for non-restricted content).
5656+- **Adult content gate**: labels marked `adultOnly: true` require the adult content master toggle to be on. If off, content is hidden with no reveal option.
5757+5858+### Moderation Preferences Storage
5959+6060+```sql
6161+-- Per-account moderation preferences (JSON blob)
6262+-- Key format: moderation_preferences::{accountDid}
6363+-- Stored in app_settings table
6464+```
6565+6666+### Labeler Cache
6767+6868+New table:
6969+7070+```sql
7171+CREATE TABLE IF NOT EXISTS labeler_cache (
7272+ labeler_did TEXT PRIMARY KEY,
7373+ policies_json TEXT NOT NULL,
7474+ fetched_at INTEGER NOT NULL
7575+);
7676+```
7777+7878+Policies are fetched on login and periodically refreshed. The cache enables offline moderation.
7979+8080+### UI Components
8181+8282+**ModeratedBlurOverlay** - wraps any content that may need blurring.
8383+8484+- 14px Gaussian blur + semi-transparent overlay
8585+- `i-ri-eye-off-line` icon + label name
8686+- "Show content" button (only if content is revealable, not restricted)
8787+- Used on: post bodies, image embeds, video embeds, quoted posts
8888+8989+**ModeratedAvatar** - wraps avatar images.
9090+9191+- Shows `i-ri-shield-line` icon when avatar is hidden
9292+- Falls through to normal avatar when no moderation applies
9393+9494+**ModerationBadgeRow** - inline badges on posts/profiles.
9595+9696+- Alert tone (red): content warnings, blocks
9797+- Inform tone (blue): informational labels
9898+- Shows label source (labeler name)
9999+100100+**ReportDialog** - modal for reporting content.
101101+102102+- Subject: post URI or profile DID
103103+- Reason type: spam, violation, misleading, sexual, rude, other
104104+- Optional free-text reason
105105+- Calls `com.atproto.moderation.createReport` via backend
106106+107107+### Store-Specific Behavior
108108+109109+**Mac App Store**: Adult content toggle is NOT available in the app. Users must enable it via Bluesky web settings (`bsky.app/settings/content-moderation`). The app reads the preference from the user's Bluesky account preferences (`app.bsky.actor.getPreferences`). A link to the web settings is shown in the moderation settings section. This satisfies Apple Guideline 1.2.
110110+111111+**GitHub / Microsoft Store**: Adult content toggle is available in-app within the moderation settings section.
112112+113113+### Settings Keys
114114+115115+| Key | Type | Default | Description |
116116+| ------------------------ | ---- | ------- | ----------------------------------------- |
117117+| `moderation_preferences` | JSON | `{}` | Per-account labeler + label prefs (keyed) |
118118+119119+The moderation preferences JSON contains: `adultContentEnabled` (bool), `subscribedLabelers` (array of DIDs), and per-labeler label visibility overrides.
120120+121121+### Moderation Settings Section
122122+123123+New section in Settings panel, between "Notifications" and "Search & Embeddings":
124124+125125+- **Adult content**: toggle (hidden on MAS builds; shows link to web settings instead)
126126+- **Subscribed labelers**: list with add/remove. Built-in Bluesky labeler shown but not removable. Max 20 custom labelers.
127127+- **Label preferences**: expandable per labeler, three-way control per label (ignore / warn / hide). Labels marked `adultOnly` are gated behind the adult content toggle.
128128+129129+## Conditional Support Button
130130+131131+A "Support Lazurite" link in the About section of Settings. Gated by distribution channel:
132132+133133+```ts
134134+// Frontend check
135135+const channel = await invoke<string>("get_distribution_channel");
136136+const showSupport = channel === "github";
137137+```
138138+139139+On GitHub builds: shows a heart icon link to the sponsorship/support page.
140140+On store builds: omitted entirely (Apple and Microsoft have their own monetization rules).
141141+142142+## App Identity
143143+144144+| Field | Value |
145145+| ------------ | ---------------------------- |
146146+| Product name | Lazurite |
147147+| Identifier | `com.owais.lazurite` |
148148+| Category | Social Networking |
149149+| Age rating | 17+ (user-generated content) |
150150+| Copyright | Copyright 2026 Owais |
151151+152152+## macOS Distribution
153153+154154+### Direct (GitHub Release)
155155+156156+- Developer ID certificate for signing
157157+- Notarization via `notarytool`
158158+- Universal binary (`x86_64 + aarch64`)
159159+- Output: `.dmg`
160160+161161+### Mac App Store
162162+163163+- Apple Distribution certificate + Mac App Store provisioning profile
164164+- Separate entitlements file (`Entitlements.mac-app-store.plist`):
165165+166166+```xml
167167+<?xml version="1.0" encoding="UTF-8"?>
168168+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
169169+<plist version="1.0">
170170+<dict>
171171+ <key>com.apple.security.app-sandbox</key> <true/>
172172+ <key>com.apple.security.network.client</key> <true/>
173173+ <key>com.apple.security.files.downloads.read-write</key> <true/>
174174+</dict>
175175+</plist>
176176+```
177177+178178+- Separate Tauri config overlay (`tauri.mac-app-store.conf.json`) merged at build time
179179+- `codesign --force --options runtime --entitlements` before `productbuild`
180180+- No notarization needed (Apple handles during review)
181181+182182+## Windows Distribution
183183+184184+### Direct (GitHub Release)
185185+186186+- NSIS installer with start menu + desktop shortcuts
187187+- Optional code signing (OV certificate)
188188+- Output: `.exe`
189189+190190+### Microsoft Store
191191+192192+- MSIX package via `winappCli` wrapping the Tauri build
193193+- No self-signing needed (Microsoft signs on upload)
194194+- IARC age rating questionnaire completed in Partner Center
195195+- Published Terms of Service and Privacy Policy required
196196+197197+## Linux Distribution
198198+199199+- AppImage (primary), `.deb`, `.rpm`
200200+- Desktop entry with `at://` MIME type
201201+- GitHub Release only (store submissions are parking lot)
202202+203203+## Auto-Update (`tauri-plugin-updater`)
204204+205205+- Update endpoint: GitHub Releases (`latest.json`)
206206+- Check on app launch + configurable periodic check
207207+- Update notification with changelog, install-on-quit option
208208+- Update bundles signed with Tauri keypair
209209+- Disabled on Mac App Store and Microsoft Store builds (stores handle updates)
210210+211211+## CI/CD - GitHub Actions
212212+213213+Matrix build with three tracks:
214214+215215+### GitHub Release Track
216216+217217+Trigger: push to `release/*` or `workflow_dispatch`.
218218+219219+| Job | OS | Output |
220220+| ------- | ---------------- | ------------------------------------ |
221221+| macOS | `macos-latest` | Universal `.dmg` (signed, notarized) |
222222+| Windows | `windows-latest` | NSIS `.exe` (signed) |
223223+| Linux | `ubuntu-latest` | AppImage, `.deb`, `.rpm` |
224224+225225+Artifacts uploaded to GitHub Release (draft) with SHA256 checksums. Generates `latest.json` for updater.
226226+227227+### Mac App Store Track
228228+229229+Trigger: manual `workflow_dispatch` with `target: mas`.
230230+231231+- Builds with `tauri.mac-app-store.conf.json` overlay
232232+- Signs with Apple Distribution certificate
233233+- Packages with `productbuild`
234234+- Uploads `.pkg` to App Store Connect via `xcrun altool` or Transporter
235235+236236+### Microsoft Store Track
237237+238238+Trigger: manual `workflow_dispatch` with `target: msstore`.
239239+240240+- Builds standard Tauri NSIS output
241241+- Wraps in MSIX via `winappCli`
242242+- Uploads to Partner Center (manual or via Store API)
243243+244244+## Required Secrets
245245+246246+| Secret | Used by |
247247+| ------------------------------------ | --------------- |
248248+| `APPLE_CERTIFICATE` | macOS signing |
249249+| `APPLE_CERTIFICATE_PASSWORD` | macOS signing |
250250+| `APPLE_ID` | Notarization |
251251+| `APPLE_PASSWORD` | Notarization |
252252+| `APPLE_TEAM_ID` | Notarization |
253253+| `APPLE_DISTRIBUTION_CERTIFICATE` | MAS signing |
254254+| `APPLE_PROVISIONING_PROFILE` | MAS builds |
255255+| `WINDOWS_CERTIFICATE` | Windows signing |
256256+| `WINDOWS_CERTIFICATE_PASSWORD` | Windows signing |
257257+| `TAURI_SIGNING_PRIVATE_KEY` | Update signing |
258258+| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Update signing |
259259+260260+## Legal Requirements (Both Stores)
261261+262262+- Published Terms of Service
263263+- Published Privacy Policy
264264+- In-app contact information
265265+- Content reporting mechanism (24-hour response for Apple)
266266+- User blocking functionality
267267+- Proactive content moderation via label system
268268+269269+## Smoke Test
270270+271271+- Fresh install: download → install → first-launch welcome
272272+- OAuth login: loopback flow on each platform
273273+- Timeline: feed rendering, scroll, keyboard shortcuts
274274+- NSFW: labeled content is blurred by default, reveal works, adult-only hidden when disabled
275275+- Reporting: submit report flow completes
276276+- Search sync: FTS5 + embedding pipeline post-login
277277+- Auto-update: detection + install from prior version (GitHub builds only)
278278+- Deep links: `at://` URI opens app and navigates
279279+- Multicolumn: column persistence across restart
280280+- Store-specific: support button visible on GitHub, hidden on store builds
+90-48
docs/tasks/13-release.md
···2233## Overview
4455-Cross-platform build, signing, packaging, and auto-update pipeline targeting macOS, Windows, and Linux. All packaging uses `tauri build` with platform-specific configuration. CI/CD runs on GitHub Actions with separate jobs per platform.
55+Cross-platform build, signing, packaging, and distribution targeting GitHub Releases, Mac App Store, and Microsoft Store. Content moderation (NSFW blurring, reporting, blocking) is a prerequisite for store submission. See [release spec](../specs/release.md) for full details.
6677## Steps
8899+### Content Moderation
1010+1111+#### Backend
1212+1313+- [ ] `ModerationService` in Rust - fetch labeler policies, evaluate labels into `ModerationDecision`, cache in `labeler_cache` table
1414+- [ ] Send `atproto-accept-labelers` header with all API requests (built-in Bluesky labeler + user-subscribed labelers)
1515+- [ ] Moderation preferences storage - per-account JSON in `app_settings` keyed by `moderation_preferences::{did}`
1616+- [ ] `create_report` command - calls `com.atproto.moderation.createReport`
1717+- [ ] `get_distribution_channel` command - returns compile-time `DISTRIBUTION_CHANNEL` env var
1818+1919+#### Frontend
2020+2121+- [ ] `ModeratedBlurOverlay` component - 14px blur, overlay icon, "Show content" button, label display
2222+- [ ] `ModeratedAvatar` component - shield icon fallback for hidden avatars
2323+- [ ] `ModerationBadgeRow` component - alert (red) and inform (blue) badges with label source
2424+- [ ] `ReportDialog` modal - reason type selector, free-text, submit
2525+- [ ] Wire moderation into `PostCard`, image/video embeds, profile views, notifications
2626+- [ ] Moderation Settings section - adult content toggle (hidden on MAS, link to web settings instead), labeler management, per-label preferences
2727+- [ ] Block user flow via `app.bsky.graph.block`
2828+929### App Identity & Branding
10301131- [x] Final app icon set: generate all required sizes from source SVG
1212- - macOS: `icon.icns` (16–1024px)
1313- - Windows: `icon.ico` (16–256px)
1414- - Linux: `icon.png` at 32, 128, 256, 512px
1515-- [ ] Update `tauri.conf.json` - `productName`, `identifier`, window title, bundle metadata (description, copyright, category)
3232+- [ ] Update `tauri.conf.json` - `productName: "Lazurite"`, `identifier: "com.owais.lazurite"`, window title, bundle metadata (description, copyright, category)
1633- [ ] Splash / welcome screen for first-launch flow
3434+- [ ] Conditional support button in About section (visible on `github` channel only)
17351818-### macOS
3636+### macOS - Direct (GitHub Release)
3737+3838+- [ ] Code signing via Developer ID certificate
3939+- [ ] Notarization via `notarytool`
4040+- [ ] DMG packaging
4141+- [ ] Universal binary (`x86_64 + aarch64`) via `--target universal-apple-darwin`
4242+- [ ] Verify Gatekeeper passes on clean install
19432020-- [ ] Code signing via Apple Developer certificate (`APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD` secrets)
2121-- [ ] Notarization via `notarytool` (`APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID` secrets)
2222-- [ ] DMG packaging - `tauri build` produces `.dmg` by default on macOS
2323-- [ ] Universal binary (x86_64 + aarch64) via `--target universal-apple-darwin`
2424-- [ ] Verify Gatekeeper passes on clean macOS install
4444+### macOS - App Store
25452626-### Windows
4646+- [ ] Apple Distribution certificate + provisioning profile
4747+- [ ] Sandbox entitlements file (`com.apple.security.app-sandbox`, `network.client`, `files.downloads.read-write`)
4848+- [ ] Separate Tauri config overlay (`tauri.mac-app-store.conf.json`)
4949+- [ ] `codesign --force --options runtime --entitlements` before `productbuild`
5050+- [ ] Adult content toggle disabled in MAS build (reads preference from Bluesky account prefs only)
5151+- [ ] Age rating: 17+
5252+- [ ] Submit via App Store Connect
27532828-- [ ] NSIS installer - `tauri build` default on Windows; configure install path, start menu shortcut, desktop shortcut
2929-- [ ] Optional: MSI installer via `bundle > targets` configuration
3030-- [ ] Code signing via certificate (`WINDOWS_CERTIFICATE`, `WINDOWS_CERTIFICATE_PASSWORD` secrets)
3131- - Evaluate EV vs OV certificate for SmartScreen reputation
3232-- [ ] Portable `.exe` variant (no install required) via WiX or NSIS portable config
3333-- [ ] Verify Windows Defender / SmartScreen does not flag the installer
5454+### Windows - Direct (GitHub Release)
5555+5656+- [ ] NSIS installer - install path, start menu shortcut, desktop shortcut
5757+- [ ] Code signing via OV certificate
5858+- [ ] Portable `.exe` variant
5959+- [ ] Verify Windows Defender / SmartScreen does not flag installer
6060+6161+### Windows - Microsoft Store
6262+6363+- [ ] MSIX packaging via `winappCli`
6464+- [ ] IARC age rating questionnaire
6565+- [ ] Submit via Partner Center
6666+- [ ] Adult content toggle available in-app (Microsoft allows)
34673568### Linux
36693737-- [ ] AppImage packaging - portable, no-install binary (primary distribution format)
3838-- [ ] `.deb` package for Debian/Ubuntu - configure dependencies (libwebkit2gtk, libssl)
3939-- [ ] `.rpm` package for Fedora/RHEL
4040-- [ ] Desktop entry file with icon, categories, and MIME type for `at://` deep links
4141-- [ ] Verify launch on Ubuntu 22.04+, Fedora 38+, and Arch (via AppImage)
7070+- [ ] AppImage (primary portable format)
7171+- [ ] `.deb` for Debian/Ubuntu (dependencies: libwebkit2gtk, libssl)
7272+- [ ] `.rpm` for Fedora/RHEL
7373+- [ ] Desktop entry with icon, categories, `at://` MIME type
7474+- [ ] Verify on Ubuntu 22.04+, Fedora 38+, Arch
42754376### Auto-Update - `tauri-plugin-updater`
44774545-- [ ] Add `tauri-plugin-updater` to `Cargo.toml` dependencies (currently commented out)
4646-- [ ] Configure update endpoint pointing to GitHub Releases (`latest.json` / release assets)
4747-- [ ] Implement update check on app launch + periodic background check (configurable in Settings)
4848-- [ ] Update available notification with changelog summary, install-on-quit option
4949-- [ ] Differential updates where supported (Tauri v2 update mechanism)
5050-- [ ] Signing update bundles with Tauri's update keypair (`TAURI_SIGNING_PRIVATE_KEY`, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`)
7878+- [ ] Add `tauri-plugin-updater` to `Cargo.toml`
7979+- [ ] Configure endpoint pointing to GitHub Releases (`latest.json`)
8080+- [ ] Update check on launch + periodic background check
8181+- [ ] Update notification with changelog, install-on-quit
8282+- [ ] Signing update bundles with Tauri keypair
8383+- [ ] Disabled on MAS and Microsoft Store builds (stores handle updates)
51845285### CI/CD - GitHub Actions
53865454-- [ ] Matrix build workflow: `[macos-latest, windows-latest, ubuntu-latest]`
5555- - macOS job: build universal binary, sign, notarize, produce `.dmg`
5656- - Windows job: build NSIS installer, sign, produce `.exe`
5757- - Linux job: build AppImage, `.deb`, `.rpm`
5858-- [ ] Trigger: push to `release/*` branch or manual `workflow_dispatch`
5959-- [ ] Upload all artifacts to GitHub Release (draft) with checksums
6060-- [ ] Generate `latest.json` manifest for `tauri-plugin-updater`
6161-- [ ] Version bump automation: tag-based versioning synced to `tauri.conf.json` and `Cargo.toml`
8787+- [ ] **GitHub Release track**: matrix `[macos-latest, windows-latest, ubuntu-latest]`, triggered on `release/*` push or `workflow_dispatch`
8888+ - macOS: universal `.dmg`, signed, notarized
8989+ - Windows: NSIS `.exe`, signed
9090+ - Linux: AppImage, `.deb`, `.rpm`
9191+ - Upload artifacts + checksums to GitHub Release (draft)
9292+ - Generate `latest.json` for updater
9393+- [ ] **Mac App Store track**: manual `workflow_dispatch`, builds with MAS config overlay, signs with Apple Distribution cert, uploads `.pkg`
9494+- [ ] **Microsoft Store track**: manual `workflow_dispatch`, wraps in MSIX via `winappCli`, uploads to Partner Center
9595+9696+### Legal
9797+9898+- [ ] Terms of Service (published, linked in app and store listings)
9999+- [ ] Privacy Policy (published, linked in app and store listings)
100100+- [ ] In-app contact information
6210163102### Smoke Test
641036565-- [ ] Fresh install flow: download, install, first-launch welcome
6666-- [ ] OAuth login: full loopback flow on each platform
6767-- [ ] Timeline load: verify feed rendering, scroll, keyboard shortcuts
6868-- [ ] Search sync: confirm FTS5 + embedding pipeline runs post-login
6969-- [ ] Auto-update: verify update detection and installation from a prior version
7070-- [ ] Deep links: `at://` URI opens app and navigates to explorer view
7171-- [ ] Multicolumn: verify column persistence across app restart
104104+- [ ] Fresh install flow on each platform
105105+- [ ] OAuth login: loopback flow on macOS, Windows, Linux
106106+- [ ] Timeline load: feed rendering, scroll, keyboard shortcuts
107107+- [ ] NSFW moderation: labeled content blurred by default, reveal works, adult-only gated
108108+- [ ] Report + block flows complete
109109+- [ ] Search sync: FTS5 + embeddings post-login
110110+- [ ] Auto-update: detection + install from prior version (GitHub builds)
111111+- [ ] Deep links: `at://` URI opens app and navigates
112112+- [ ] Multicolumn: column persistence across restart
113113+- [ ] Store-specific: support button on GitHub, hidden on store; adult toggle on GitHub/MS, hidden on MAS
7211473115### Parking Lot
741167575-- [ ] Flathub / Snap Store submission for Linux
7676-- [ ] Windows Store (MSIX) submission
7777-- [ ] macOS App Store submission
7878-- [ ] Crash reporting integration (Sentry or similar)
117117+- [ ] Flathub / Snap Store submission
118118+- [ ] macOS: separate DMG + App Store builds in single CI run
119119+- [ ] Crash reporting (Sentry or similar)
79120- [ ] Analytics / telemetry (opt-in, privacy-respecting)
80121- [ ] Beta / nightly release channel
122122+- [ ] Differential updates (Tauri v2 update mechanism)