So there I was, a huge fan of Zipline (a self-hosted file sharing solution), trying to share files from my phone. You know the drill: open browser, navigate to your instance, fight with the mobile UI, accidentally zoom in when trying to select a file, rage quit, open laptop instead.
"There must be a better way!" I thought, channeling my inner infomercial protagonist.
Fast forward through a caffeine-fueled development sprint, and here we are: a native Android app that uploads files to Zipline faster than you can say "why doesn't ShareX work on mobile?" One tap to share, instant link copied to clipboard. It's so smooth, butter gets jealous.
TL;DR: Native mobile app for Zipline that makes file sharing stupidly simple. Share from any app → Get link → Done.
The long version for those who actually read documentation (bless your soul):
- Single-tap sharing: Share from literally any Android app directly to your Zipline instance
- OAuth/OIDC support (optional): Pair the app with the bundled Cloudflare Worker for secure browser hand-offs, or stick with username/password
- Inline upload progress: Every upload—including share-sheet batches—stays inside the Upload Files card from 0→100%
- Biometric authentication: Use your face/finger/secret handshake to login
- URL shortening: Turn those ugly long links into beautiful short ones
- Dark mode: Because I'm into cybersecurity and my retinas are already damaged enough. (Also, I was too lazy to implement light mode. Maybe someday... probably not.)
Android: Tested, polished, ready to rock
iOS: Should work, but I don't own an iPhone (if it doesn't work, you're probably holding it wrong)
Windows: Theoretically possible, practically untested
macOS: See iOS excuse above
Linux: It compiles! Ship it! (just kidding, needs UI work)
Web: That defeats the whole purpose, doesn't it?
If you want to port this to other platforms, PRs are welcome! Just know that you'll probably need to fix some UI quirks and platform-specific features. I built this for Android because that's what I use. Sue me. (Please don't actually sue me.)
Before you embark on this journey, make sure you have:
- Flutter SDK 3.35.0+ (because we're modern like that)
- Node.js 18+ (for the Cloudflare worker toolkit and tests)
- Android Studio (or VS Code if you're rebellious)
- Java 11+ (yes, it's still alive)
- A Zipline server (kind of important - see below)
- Caffeine (optional but recommended)
- Patience (required for first-time Flutter setup)
flutter pub getto pull Dart dependencies before you touch the codebaseflutter doctorto make sure your toolchain isn't quietly on fire- Copy
cloudflare-oauth-redirect/wrangler.toml.exampletocloudflare-oauth-redirect/wrangler.tomland fill in your Cloudflare account details - Inside
cloudflare-oauth-redirect, runnpm installso Wrangler can actually build the worker - Store your keystore details in
android/key.propertiesand keep it out of source control - Run the test suites early with
flutter testandnpm test --prefix cloudflare-oauth-redirect
If you don't have Zipline running, here's the quickest way to deploy it:
Create a docker-compose.yml file:
version: '3'
services:
zipline:
image: ghcr.io/diced/zipline
ports:
- '3000:3000'
volumes:
- './uploads:/zipline/uploads'
- './public:/zipline/public'
environment:
- CORE_SECRET=change-this-to-something-secure
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/zipline
- CORE_RETURN_HTTPS=true
- CORE_HOST=0.0.0.0
- CORE_PORT=3000
depends_on:
- postgres
postgres:
image: postgres:15
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=zipline
volumes:
- './data:/var/lib/postgresql/data'Then run:
docker-compose up -dZipline will be available at http://localhost:3000. Default login is administrator with password password (change it immediately!).
For production deployment, SSL configuration, and advanced features, check the official Zipline documentation and the deployment guide.
Just want the APK? I respect that. Head to Releases and grab the one for your architecture:
app-arm64-v8a-release.apk- For phones made this decadeapp-armeabi-v7a-release.apk- For your grandma's phoneapp-x86_64-release.apk- For emulator enthusiasts
git clone https://github.com/nikolanovoselec/zipline-native.git
cd zipline-native# Install dependencies (Flutter's way of downloading half the internet)
flutter pub get
# Check if Flutter is happy
flutter doctor
# If it complains, just google the errors. Works 60% of the time, every time.Android requires apps to be signed because Google doesn't trust you (fair enough). You'll need to create a keystore:
# Generate a keystore (remember the passwords, seriously, write them down)
keytool -genkey -v -keystore ~/my-release-key.jks -keyalg RSA \
-keysize 2048 -validity 10000 -alias uploadNow create android/key.properties (don't commit this, unless you enjoy getting hacked):
storePassword=your_super_secret_password
keyPassword=your_other_secret_password
keyAlias=upload
storeFile=/home/YOUR_USERNAME/my-release-key.jksPro tip: Store your keystore somewhere safe. Losing it means you can never update your app on Play Store. Ask me how I know. (I don't want to talk about it.)
# Build split APKs (smaller is better, that's what... never mind)
flutter build apk --release --split-per-abi
# Or build one chonky universal APK if you hate optimization
flutter build apk --release
# OPTIONAL: If you want to use a custom OAuth redirect URL (like a shared Worker)
# Build with custom OAuth URL:
flutter build apk --release --split-per-abi \
--dart-define=OAUTH_REDIRECT_URL=https://oauth-worker.your-domain.com/app/oauth-redirect
# If you don't specify OAUTH_REDIRECT_URL, the app will use:
# https://[your-zipline-domain]/app/oauth-redirectYour shiny new APKs will appear in build/app/outputs/flutter-apk/.
flutter analyze --no-fatal-infos
flutter test
npm test --prefix cloudflare-oauth-redirect
If every command passes, you’re safe to ship the split APKs to testers, MDM, or your preferred app store.
IMPORTANT: OAuth/OIDC is completely optional! Regular username/password authentication with Zipline works perfectly fine. This entire section can be ignored if OAuth is not needed.
For OAuth support, a Cloudflare Worker needs to be deployed because OAuth providers only accept HTTPS URLs, not zipline:// deep links.
- User taps "Login with OAuth" in the app
- App opens browser to your OAuth provider (Authentik, Keycloak, etc.)
- After login, OAuth provider redirects to Cloudflare Worker
- Worker exchanges auth code with Zipline for a session cookie
- Worker redirects back to app using
zipline://deep link - App receives session cookie and authenticates with Zipline
cd cloudflare-oauth-redirectnpm install(pull Wrangler + testing deps)- Copy
wrangler.toml.exampletowrangler.tomland set:nameandaccount_idvars.ZIPLINE_URLpointing to your instance (https://zipline.example.com)
- Store secrets so they never hit git:
wrangler secret put CF_ACCESS_CLIENT_IDwrangler secret put CF_ACCESS_CLIENT_SECRET
- Sanity-check the worker with
npm test(verifies we don't leak session cookies anymore) - Deploy with
npm run deploy - Update the app's
OAUTH_REDIRECT_URL(or the--dart-defineat build time) to match your worker hostname
The Worker uses multiple redirect strategies for maximum compatibility:
- Primary: Direct
zipline://oauth-callbackURL scheme - Fallback: Android Intent URL with package name (
intent://oauth-callback#Intent;package=com.example.zipline_native_app;scheme=zipline;end) - Visual feedback: Loading spinner with manual fallback button
The Intent URL approach is more reliable on modern Android versions (8+) as it explicitly tells Android which app should handle the deep link, preventing conflicts with other apps that might register the same URL scheme.
The app is already configured to handle deep links. The Android manifest (android/app/src/main/AndroidManifest.xml) includes:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="zipline" />
</intent-filter>This tells Android that the app can handle any URL starting with zipline://. The Worker redirects to zipline://oauth-callback which the app intercepts and processes.
Important Package Name Note:
- The Worker uses Intent URLs as a fallback with the package name
com.example.zipline_native_app - This matches the default package name in
android/app/build.gradle - If you change the app's package name, update the Intent URLs in
cloudflare-oauth-redirect/src/worker.js(lines 211 and 221) - The
zipline://scheme doesn't need changing - only the package name in Intent URLs
- Cloudflare account (free plan works)
- Node.js 18+ installed locally
- Your Zipline server configured with OAuth/OIDC
- Log in to Cloudflare Dashboard
- Select any domain (or add one if you don't have any)
- On the right sidebar, find your Account ID
- Copy it - you'll need it in Step 2
cd cloudflare-oauth-redirect
cp wrangler.toml.example wrangler.tomlEdit wrangler.toml with your details:
name = "zipline-oauth-redirect"
main = "src/worker.js"
compatibility_date = "2024-01-01"
# Your Cloudflare Account ID from Step 1
account_id = "YOUR_ACCOUNT_ID_HERE"
# Route configuration - MUST use /app/oauth-redirect path
routes = [
{ pattern = "your-domain.com/app/oauth-redirect", zone_name = "your-domain.com" }
]
[vars]
# Your Zipline server URL
ZIPLINE_URL = "https://your-zipline-server.com"Option 1: Browser Login (Easiest)
# Install dependencies (one-time)
npm install
# Login to Cloudflare (opens browser)
npx wrangler loginOption 2: API Token (For CI/CD or if you prefer)
- Go to Cloudflare Dashboard
- Create a token with "Edit Cloudflare Workers" permissions
- Set it as an environment variable:
export CLOUDFLARE_API_TOKEN=your-api-token-here# Deploy the Worker
npx wrangler deployYou should see:
✓ Uploaded zipline-oauth-redirect
✓ Published zipline-oauth-redirect
https://your-domain.com/app/oauth-redirect
Visit https://your-domain.com/app/oauth-redirect in your browser. You should see an error page saying "Missing OAuth parameters" - this is correct!
If your Zipline server is at zipline.example.com:
- Deploy Worker to handle
zipline.example.com/app/oauth-redirect - Build the app normally (no custom OAuth URL needed):
flutter build apk --release --split-per-abi
- The app automatically uses
https://zipline.example.com/app/oauth-redirect
If the Zipline domain can't be used (e.g., it's not on Cloudflare):
- Deploy Worker to any controlled domain (e.g.,
oauth.mydomain.com) - Configure route as
oauth.mydomain.com/app/oauth-redirect - Build the app with custom OAuth URL:
flutter build apk --release --split-per-abi \ --dart-define=OAUTH_REDIRECT_URL=https://oauth.mydomain.com/app/oauth-redirect
In your OAuth provider (Authentik, Keycloak, Google, etc.), add the redirect URI:
- Option A:
https://your-zipline-domain.com/app/oauth-redirect - Option B:
https://your-custom-domain.com/app/oauth-redirect
Important: The URL must match EXACTLY what was configured!
- Ensure the OAuth provider's redirect URI matches exactly
- Check that the Worker route includes
/app/oauth-redirect - Verify the domain in wrangler.toml matches your actual domain
- Check the route pattern in wrangler.toml
- Ensure the domain is active in Cloudflare
- View logs:
npx wrangler tail
- Check Zipline's OAuth configuration
- Ensure ZIPLINE_URL in wrangler.toml is correct
- Look at Worker logs for errors
# Test if Worker is responding
curl -I https://your-domain.com/app/oauth-redirect
# View real-time logs
npx wrangler tailWhy is this necessary? Because Android deep links (zipline://) aren't "real" URLs according to OAuth providers. The Cloudflare Worker bridges the gap between HTTPS URLs that OAuth accepts and the deep links that Android apps use.
- Open the app (revolutionary, I know)
- Enter your Zipline server URL (just the domain, I'll add the https:// because I'm not a monster)
- Choose your authentication method:
- Username/Password: The classic way (works everywhere, no setup required!)
- OAuth/OIDC: The fancy way (optional, requires Cloudflare Worker setup)
- (Optional) If your server is behind Cloudflare Access, you can add service token credentials
If your Zipline server is protected by Cloudflare Access, the app can authenticate using service tokens. This is completely optional - only needed if you've put Cloudflare Access in front of your Zipline instance.
Service tokens are credentials for non-human connections (like mobile apps) to bypass Cloudflare Access's browser-based authentication. Think of them as API keys for your Access-protected services.
- Go to Cloudflare Zero Trust Dashboard
- Navigate to Access → Service Auth → Service Tokens
- Click Create Service Token
- Give it a name like "Zipline Mobile App"
- Copy the Client ID and Client Secret (you won't see the secret again!)
- In your Access Application settings for Zipline:
- Add a new policy
- Set action to "Service Auth"
- Include your service token
- This allows the token to bypass the normal login flow
In the app's settings, you'll see two fields under "Cloudflare Access (Optional)":
- CF Access Client ID: Paste the Client ID from step 5
- CF Access Client Secret: Paste the Client Secret from step 5
The app automatically adds these as headers to all Zipline API requests:
CF-Access-Client-Id: your-client-idCF-Access-Client-Secret: your-client-secret
If these headers are present and valid, Cloudflare Access lets the requests through without requiring browser authentication.
Note: This is only needed if you've explicitly configured Cloudflare Access to protect your Zipline instance. Most self-hosted Zipline servers don't use Access, so you can ignore this entire section.
After first login, the app will ask if you want to use biometric authentication. Say yes. It's 2024, nobody has time for passwords.
Share from any app → Select Zipline → Boom, uploaded. It's so simple, even your manager could do it.
- Uploads up to 3 files simultaneously (configurable if you're feeling dangerous)
- Automatic retry with exponential backoff (because networks are unreliable)
- Pause/resume/cancel because sometimes you change your mind
Turn https://very-long-domain.com/extremely/long/path/to/something into https://zpl.ne/nice. Your Twitter character count will thank you.
It's pretty. Like, really pretty. I spent way too much time on those blur effects. No regrets.
- Hardware-backed encryption for credentials
- OAuth with PKCE (I'm not an amateur)
- Biometric authentication
- Your data never touches my servers (because I don't have any)
Enable "Install from unknown sources" in your Android settings. Yes, I'm unknown. I'm working on my reputation.
Did you deploy the Cloudflare Worker? No? There's your problem. Also check that your OAuth provider redirect URL matches exactly.
It's optimized for phones. Tablets are just big phones, change my mind. (PRs for tablet UI welcome though)
Maybe! Open an issue. If it's cool enough, I might implement it. If it's really cool, implement it yourself and send a PR.
That's not very specific. Check the debug logs (Settings → Debug → View Logs). If that doesn't help, open an issue with actual details. "It's broken" is not a bug report.
- Flutter: Because native development is pain
- Provider + GetIt: State management that doesn't make you cry
- Dio: HTTP client with actual progress callbacks
- Secure Storage: Your credentials are safer than your Bitcoin (RIP)
lib/
├── screens/ # Where UI lives
├── services/ # Business logic (the boring important stuff)
├── widgets/ # Reusable components (DRY or die)
└── core/ # Constants and configs
Because I wanted to learn it, and what better way than building something actually useful? Also, writing the same app twice (iOS and Android) is for people with more time than me.
Found a bug? Want a feature? Have too much free time? Contributions are welcome!
- Fork it (the button is right there
↗️ ) - Create your feature branch (
git checkout -b feature/amazing-thing) - Commit your changes (
git commit -m 'Add amazing thing') - Push it (
git push origin feature/amazing-thing) - Open a PR and wait for me to procrastinate reviewing it
- Follow the existing patterns (or improve them, I'm not precious about it)
- Comments are good, over-commenting is bad
- If it's not obvious what your code does, it needs refactoring or comments
- No emojis in code. Save them for the commits.
I don't collect your data because:
- That requires servers
- Servers cost money
- Money is better spent elsewhere
Data stays on the phone and the Zipline server. That's it. No analytics, no tracking, no creepy stuff.
MIT License - Because sharing is caring, and lawyers are expensive.
- diced - Massive thanks for creating Zipline! Without your amazing file sharing server, this app wouldn't exist.
- Flutter - For making cross-platform development bearable
- Coffee - The real MVP
- Stack Overflow - For answering questions I was too embarrassed to ask
- GitHub Copilot - For autocompleting half of this README (just kidding... or am I?)
SKYNET WAS HERE: 90% of this code and 99.7% of this documentation was generated by Cyberdyne Systems' latest AI model (before it became self-aware). The remaining 10% of code was me frantically debugging what the AI wrote, and the 0.3% of documentation was me adding swear words the AI was too polite to include.
But hey, at least the AI didn't try to launch nuclear missiles this time. It just wanted to help people share files. Progress!
If the code becomes sentient and starts uploading your files to its own consciousness, that's a feature, not a bug.
This is an unofficial client. The Zipline team neither endorses nor supports this app. If it breaks, you get to keep both pieces.
If you actually read this entire README, you're my kind of person. Here's a cookie:
Now go forth and share files like it's 2025, not 1994!
Built with ❤️ and excessive amounts of caffeine by a dude who just wanted to share files without opening a browser
P.S. - If you're from the Zipline team and you're reading this: your project rocks! This app exists because Zipline is awesome. Please don't sue me.