Thank you for your interest in contributing to Substreamer! This guide covers everything you need to get started.
This project includes pre-configured instruction files for AI coding assistants:
| Tool | Instruction File |
|---|---|
| Cursor | .cursor/rules/project-overview.mdc (+ additional rule files in .cursor/rules/) |
| GitHub Copilot | .github/copilot-instructions.md |
| Claude Code | CLAUDE.md |
These files contain project conventions, architecture details, and coding standards so your AI assistant understands the codebase. If you use one of these tools, the rules will be picked up automatically — no configuration needed.
Important: All three instruction files must stay in sync. If you update one, apply the same change to the other two.
When working with Claude Code, you can share your terminal output so the AI can see your command results. Run npm run terminal-log in any terminal you want observed — this starts a script session that logs everything to /tmp/claude-terminal.txt. Then ask Claude Code to "check my terminal" to have it read the output. Type exit to stop recording.
git clone https://github.com/ghenry22/substreamer.git
cd substreamer
npm install
npx expo startUse the Expo Go app on your device or an emulator/simulator to connect to the dev server.
Follow these steps in order on a fresh macOS machine. If you already have some of these installed, skip to what you need.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"Follow the post-install instructions printed in your terminal to add Homebrew to your PATH in ~/.zprofile.
Install nvm (do not install Node via Homebrew):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bashRestart your terminal, then install Node 22:
nvm install 22
nvm alias default 22Verify: node -v should show v22.x and npm -v should be present.
Ruby is required for fastlane (store metadata automation). macOS may ship with an outdated version, so install via Homebrew:
brew install ruby@3.2Add to your ~/.zshrc:
export PATH="/opt/homebrew/opt/ruby@3.2/bin:$PATH"Restart your terminal. Verify: ruby -v should show 3.2.x.
Bundler ships with modern Ruby — no separate install needed. From the project root:
bundle installThis installs fastlane and its dependencies locally to vendor/bundle. If you see a message about a bundler version mismatch from an old Gemfile.lock, delete it and re-run (Gemfile.lock is gitignored).
Install Xcode from the Mac App Store, then configure it:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license acceptCocoaPods is required for linking native dependencies in iOS builds. Install via gem (not Homebrew, to avoid Ruby version conflicts):
gem install cocoapodsVerify: pod --version should print a version number.
Note: Run
pod installdirectly — notbundle exec pod install. CocoaPods is not in the project Gemfile since Expo prebuild regenerates theios/directory and manages pod installation automatically.
Download from https://developer.android.com/studio and install to the default location (/Applications/Android Studio.app/). The build scripts expect this exact path.
On first launch, complete the setup wizard — this installs:
- Android SDK (to
~/Library/Android/sdk) - JBR 17 (bundled JDK)
- Platform tools and emulator
Then create at least one AVD (emulator) via Tools > Device Manager. The scripts/env-android.sh helper auto-launches the first available AVD when you run npm run android.
No manual JAVA_HOME or ANDROID_HOME exports are needed — the build scripts auto-detect from the default paths.
Required for creating releases (npm run release):
brew install gh
gh auth loginSelect GitHub.com, HTTPS, and authenticate via browser.
cd substreamer
npm installnpx tsc --noEmit # TypeScript — should be clean
npx jest --no-coverage # Tests — should all pass
npm run ios # iOS build (requires Xcode)
npm run android # Android build (requires Android Studio + emulator)- Node.js 22 (LTS) via nvm
- npm (bundled with Node)
- Ruby 3.2 + Bundler (for fastlane)
- Xcode (iOS builds, macOS only)
- CocoaPods via gem (iOS native dependency linking)
- Android Studio (Android builds — provides JBR 17 and Android SDK)
- GitHub CLI (for releases)
- A Subsonic-compatible server for testing (Navidrome recommended)
The android/ and ios/ directories are generated by expo prebuild and are gitignored. Never edit files inside them directly — changes are lost on the next prebuild.
Native Android builds require JAVA_HOME and ANDROID_HOME to be set. Use the helper scripts which auto-detect Android Studio's bundled JBR and the Android SDK:
| Command | What it does |
|---|---|
scripts/build-android.sh |
Sets env vars, runs npx expo run:android |
scripts/build-android.sh --gradle-only |
Sets env vars, runs ./gradlew assembleDebug |
scripts/build-android.sh --gradle-only --release |
Release variant via Gradle |
scripts/build-android.sh --no-install |
Build only, skip device install |
scripts/build-modules.sh |
Builds local native module JS/types |
For iOS builds, run npx expo run:ios directly.
| Component | Technology |
|---|---|
| Framework | Expo ~55 / React Native 0.83 |
| Language | TypeScript (strict mode) |
| Routing | Expo Router (file-based) |
| State | Zustand with SQLite persistence |
| Audio | react-native-track-player (local fork) |
| Lists | @shopify/flash-list v2 |
| Animations | react-native-reanimated v4 |
| API | Subsonic REST via subsonic-api |
TypeScript strict mode is enabled. All code must pass npx tsc --noEmit with zero errors.
| Type | Convention | Example |
|---|---|---|
| Component files | PascalCase | AlbumCard.tsx |
| Screen files | kebab-case | album-detail.tsx |
| Route files | kebab-case / [id] |
album/[id].tsx |
| Components | PascalCase | AlbumCard |
| Hooks | camelCase with use prefix |
useTheme |
| Stores | camelCase with Store suffix |
playerStore |
| Constants | UPPER_SNAKE_CASE | ROW_HEIGHT |
| Handler functions | handle prefix |
handlePress |
| Callback props | on prefix |
onPress |
Order imports as: external packages, then internal modules, then type-only imports. Use type keyword for type-only imports.
import { useRouter } from 'expo-router';
import { memo, useCallback } from 'react';
import { Pressable, StyleSheet, Text } from 'react-native';
import { CachedImage } from './CachedImage';
import { useTheme } from '../hooks/useTheme';
import { type AlbumID3 } from '../services/subsonicService';- Use
StyleSheet.create()at module scope for static styles. - Apply theme colors inline via the
useTheme()hook — never import theme constants directly. - Use
CachedImagefor all Subsonic cover art — never use raw<Image>. - All animations should use
react-native-reanimated(exception: slow linear translations likeMarqueeTextuse RNAnimated).
# TypeScript check
npx tsc --noEmit
# Run all tests
npx jest --no-coverage
# Run tests with coverage
npx jest --coverage --coverageReporters=text
# Run a specific test file with coverage
npx jest path/to/test --coverage --coverageReporters=textEvery file with a corresponding test file must maintain 80% or higher statement coverage and branch coverage.
- Null/undefined inputs and optional fields
- Error and failure paths
- Edge cases and boundary conditions
- State transitions across multiple steps
- Interactions between subsystems
Avoid tests that only confirm the trivial success case. If a function has branches, test them.
Tests run automatically on every push and pull request via GitHub Actions (.github/workflows/tests.yml).
- Branch from
master— create a descriptive branch name (e.g.fix/playback-resume,feat/shuffle-mode). - Run checks before starting —
npx tsc --noEmitandnpx jest --no-coveragemust both pass. - Make your changes — keep commits focused and atomic.
- Run checks after finishing — both must still pass. Add or update tests to cover your changes.
- Open a pull request — fill out the PR template, describe what changed and why, and link related issues.
src/
app/ # Expo Router routes (thin wrappers importing from screens/)
screens/ # Screen components with business logic
components/ # Reusable UI components
hooks/ # Custom hooks
services/ # API clients and external integrations (plain async functions)
store/ # Zustand stores (SQLite persistence via expo-sqlite)
constants/ # Theme definitions
utils/ # Formatting, color, string, and timing helpers
modules/ # Local Expo native modules
plugins/ # Expo config plugins (modify native projects during prebuild)
scripts/ # Build helper scripts
- Route/Screen separation: Route files in
app/are thin wrappers; all business logic lives inscreens/. - Zustand stores manage all app state with SQLite persistence.
- Services are plain modules exporting async functions (no classes).
CachedImageis the standard component for all cover art.useTheme()provides{ theme, colors }for all styling.
Substreamer includes a versioned data migration system that runs automatically during the animated splash screen on app launch. Migrations handle changes to stored data or cached files between app versions.
- On every launch the splash screen plays its logo animation.
- After the animation, the system checks for pending migration tasks by comparing the store's
completedVersionagainst the task registry. - If there are no pending tasks the splash fades out normally.
- If there are pending tasks the logo shrinks and slides up, a status message and progress indicator appear, tasks run sequentially. On completion a checkmark and "Update complete" message display briefly before the splash fades out.
| File | Purpose |
|---|---|
src/services/migrationService.ts |
Task definitions and runner |
src/store/migrationStore.ts |
Tracks which migration version has completed |
src/components/AnimatedSplashScreen.tsx |
Splash screen with integrated migration UI |
- Open
src/services/migrationService.ts. - Add a new entry to the
MIGRATION_TASKSarray with the next sequentialid:
{
id: 2,
name: 'Short description for the user',
run: async () => {
// Your migration logic here.
// Throw an error to signal failure.
},
},- The runner picks up any task whose
idis greater thancompletedVersionand executes them in order.
Releases are managed by a script that increments the version, updates the changelog, commits, tags, pushes, and creates a GitHub release.
Native production builds run automatically when a new release version is created.
GitHub CLI must be installed and authenticated:
brew install gh
gh auth loginnpm run release -- patch # 8.0.0 -> 8.0.1
npm run release -- minor # 8.0.0 -> 8.1.0
npm run release -- major # 8.0.0 -> 9.0.0The script will:
- Increment the version in
app.jsonandpackage.json - Collect all commits since the last release tag
- Prepend a new entry to
CHANGELOG.md - Commit the changes, create a git tag, and push to origin
- Create a GitHub release with the changelog as release notes
The working tree must be clean (no uncommitted changes) before running.