A self-hosted micro link journal. Multi-user, chronological, yours.
Live demo: https://trail.services.kibotu.net
maestro-recording.mp4
TL;DR for the impatient:
# Backend
git clone https://github.com/kibotu/trail.git && cd trail/backend
composer install && cp secrets.yml.example secrets.yml
# Edit secrets.yml, create database, then: cd .. && ./sync.sh
# Android
cd android && ./gradlew assembleDebug installDebug # Debug APK
./gradlew bundleRelease # Play Store bundlePersonal link journaling without algorithmic feeds. Share what matters, own your data, control your timeline. Built for self-hosting hobbyists who want a simple, chronological space to collect links, thoughts, and images.
What makes Trail different:
- Chronological only — No algorithms, no engagement optimization, no dark patterns. Posts appear in the order they were created. Revolutionary, we know.
- 140 characters — Constraints breed creativity. If you need more, you're probably overthinking it.
- Self-hosted first — Your server, your rules, your data. No venture capital, no pivot to ads, no "we're shutting down" emails.
- Multi-user — Not just a personal journal. Invite friends, family, or your entire homelab Discord.
- Native Android app — Because PWAs are fine, but native apps are better. Share from anywhere, post from your phone, no browser tabs.
- RSS feeds — Because some of us still use feed readers like it's 2008. And that's okay.
- 140-character posts with automatic URL card previews (powered by Iframely)
- Up to 3 images per post/comment (WebP-optimized, 20MB max each)
- Claps (Medium-style, 1-50 per entry), threaded comments, @mentions
- View counts on entries, comments, and profiles
- Per-user pages (
/@username) and global chronological feed - Full-text search with relevance ranking
- Customizable profiles (avatar, header image, bio)
- Notification system (claps, mentions)
- User muting and content reporting
- RSS feeds (global + per-user)
- Google OAuth 2.0 + persistent API tokens
- Twitter/X archive migration (one command)
- Account deletion with grace period and account restore
- Data export — download all your data
- Data Privacy and Terms & Conditions
- Profile widget (light/dark theme, transparent background, auto-resize)
- Responsive iframe with auto-height messaging
- Configurable header, search, pagination
- Share intent — Post from any app (text → Share → Trail)
- Dual feeds — Global timeline + personal
/@usernamefeed - Rich media — Images, GIFs, video playback (inline/fullscreen)
- Offline-first auth — Google Sign-In with JWT token persistence
- Material 3 — Light/dark theme, edge-to-edge, animated splash
- Baseline profiles — Optimized cold start and runtime performance
Backend: PHP 8.4+ • Slim 4 • MariaDB • JWT • Iframely API
Android: Kotlin 2.3 • Jetpack Compose • Ktor • Material 3
# 1. Clone and install
git clone https://github.com/kibotu/trail.git
cd trail/backend
composer install
# 2. Create database
mysql -u root -p
CREATE DATABASE trail_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# 3. Configure
cp secrets.yml.example secrets.yml
# Edit secrets.yml with your database, Google OAuth, and JWT settings
# 4. Deploy
cd ..
./sync.shSee backend/README.md for complete deployment instructions.
Requirements: Android 6.0+ (API 23), JDK 17, Android Studio Ladybug+ (or command-line tools)
cd android
# 1. Configure (create local.properties)
cat > local.properties << EOF
API_BASE_URL=https://your-trail-instance.example.com/
DEBUG_KEYSTORE_PATH=certificates/debug.jks
DEBUG_STORE_PASSWORD=your_password
DEBUG_KEYSTORE_ALIAS=debug
DEBUG_KEY_PASSWORD=your_password
EOF
# 2. Build & install debug APK
./gradlew assembleDebug installDebug
# 3. Build release APK (minified, R8)
./gradlew assembleRelease
# 4. Build app bundle for Play Store
./gradlew bundleReleaseOutputs:
APKs → app/build/outputs/apk/
Bundles → app/build/outputs/bundle/
Note: Also update app/src/main/res/values/strings.xml → default_web_client_id with your Google OAuth Web Client ID (from Cloud Console → Credentials).
See android/README.md for architecture details.
Generate keystores for debug and release builds:
# Debug keystore (for development)
keytool -genkey -v -keystore android/certificates/debug.jks \
-alias debug -keyalg RSA -keysize 2048 -validity 10000 \
-storepass your_password -keypass your_password
# Release keystore (for production)
keytool -genkey -v -keystore android/certificates/release.jks \
-alias release -keyalg RSA -keysize 2048 -validity 10000 \
-storepass your_password -keypass your_passwordThen update android/local.properties with the paths and passwords.
Pro tip: For CI/CD, encode keystores as base64 and store in secrets:
base64 -i certificates/release.jks | pbcopy # macOS
base64 -w 0 certificates/release.jks # LinuxMigrate your Twitter/X archive to Trail in one command:
cd twitter
./migrate.sh --api-key YOUR_API_KEY --archive twitter-backup.zipSee twitter/README.md for details.
Add your Trail feed to any website with a single iframe:
<iframe src="https://trail.services.kibotu.net/@kibotu/embed?theme=dark"
style="border:none; width:100%; min-width:320px;"
loading="lazy"
allow="web-share; clipboard-write"></iframe>| Parameter | Values | Default | Description |
|---|---|---|---|
theme |
light, dark |
dark |
Color scheme (transparent background) |
header |
0, 1 |
0 |
Show profile avatar, bio, and stats |
search |
0, 1 |
0 |
Show search bar |
limit |
1-50 |
20 |
Entries per page |
The embed posts trail-embed-resize messages for auto-height. A ready-to-copy snippet with the resize script is available on your /profile page under "Embed Your Profile".
Full REST API with public and authenticated endpoints. Generate an API token from your profile page.
Documentation: https://trail.services.kibotu.net/api
# Development
./gradlew assembleDebug # Build debug APK
./gradlew installDebug # Install debug APK to device
./gradlew uninstallDebug # Uninstall debug APK
# Release
./gradlew assembleRelease # Build release APK (minified, R8)
./gradlew bundleRelease # Build app bundle for Play Store
./gradlew installRelease # Install release APK to device
# Release with custom version (for manual builds)
./gradlew assembleRelease -PversionName=1.0.0 -PversionCode=10000001
./gradlew bundleRelease -PversionName=1.0.0 -PversionCode=10000001
# Testing
./gradlew test # Run unit tests
./gradlew connectedAndroidTest # Run instrumented tests
# Code Quality
./gradlew lint # Run Android linter
./gradlew lintDebug # Lint debug variant
./gradlew lintRelease # Lint release variant
# Baseline Profiles (performance optimization)
./gradlew generateBaselineProfile # Generate baseline profile
./gradlew :baselineprofile:pixel6Api36Setup # Setup managed device
# Note: Baseline profiles improve cold start by ~30% and reduce jank.
# They're auto-generated during release builds and committed to src/main/.
# Cleanup
./gradlew clean # Clean build artifacts
./gradlew cleanBuildCache # Clean Gradle build cache
# Diagnostics
./gradlew dependencies # Show dependency tree
./gradlew logVersionOverrides # Analyze version conflicts
./gradlew tasks --all # List all available tasksApp bundles (.aab) are the preferred format for Google Play Store distribution. They enable:
- Dynamic delivery — Users download only the APK splits for their device (density, ABI, language)
- Smaller downloads — ~15-30% smaller than universal APKs
- Automatic optimization — Google Play generates optimized APKs per device config
The bundleRelease task produces app/build/outputs/bundle/release/app-release.aab.
For local testing of bundles, use bundletool:
# Generate APKs from bundle
bundletool build-apks --bundle=app-release.aab --output=app.apks
# Install to connected device
bundletool install-apks --apks=app.apkstrail/
├── android/ # Android app (Kotlin, Compose)
│ ├── app/ # Main application module
│ ├── baselineprofile/ # Performance optimization profiles
│ └── local.properties # Config (not in git)
├── backend/ # PHP API (Slim 4)
│ ├── public/ # Web root
│ ├── src/ # Controllers, Models, Services
│ └── secrets.yml # Configuration (not in git)
├── twitter/ # Archive importer
│ ├── migrate.sh # Migration script
│ └── README.md
├── migrations/ # SQL migrations
└── sync.sh # Deployment script
Problem: AAPT: error: resource android:attr/lStar not found
Solution: Upgrade to compileSdk 31+ (app targets SDK 36)
Problem: Cleartext HTTP traffic not permitted
Solution: HTTPS is enforced by network_security_config.xml. For local dev, add your IP to the config or use ngrok/localtunnel.
Problem: Google Sign-In fails with DEVELOPER_ERROR
Solution: Verify default_web_client_id in strings.xml matches your Google Cloud Console OAuth 2.0 Web Client ID (not Android client ID).
Problem: Gradle build is slow
Solution: Already optimized with configuration cache, parallel execution, and build cache. For faster incremental builds, avoid clean unless necessary. First build takes longer (downloads dependencies, generates baseline profiles).
Problem: Configuration cache warnings
Solution: Harmless. The build uses org.gradle.configuration-cache=true for speed. Warnings are set to warn level in gradle.properties.
Problem: App bundle upload rejected by Play Console
Solution: Ensure versionCode is incremented in app/build.gradle.kts for each release.
Problem: API returns 401 Unauthorized
Solution: Check JWT token expiry. The Android app auto-refreshes on 401, but verify secrets.yml has correct jwt_secret.
Problem: Image uploads fail
Solution: Check PHP upload_max_filesize and post_max_size (should be ≥20MB). Verify write permissions on backend/public/uploads/.
Problem: Iframely previews not working
Solution: Requires paid Iframely API key in secrets.yml. Free tier has rate limits.
- Reverse proxy: Use Caddy or nginx with automatic HTTPS (Let's Encrypt). Trail expects to run at domain root or subdirectory.
- Database backups: Automate with
mysqldumpcron job. Trail stores everything in MariaDB (no filesystem dependencies except uploads). - Android app distribution: Self-hosters can distribute APKs directly (no Play Store required). Consider F-Droid or Obtainium for updates.
- Multi-instance: Each Android app can connect to different backends by changing
API_BASE_URLinlocal.propertiesand rebuilding. - Performance: Enable MariaDB query cache and PHP opcache. For high traffic, add Redis for session storage.
PRs welcome. For major changes, open an issue first. Follow existing code style (PSR-12 for PHP, Kotlin official style guide for Android).
Run tests before submitting:
# Backend
cd backend && composer test
# Android
cd android && ./gradlew test lintApache 2.0