Skip to content

rshankras/IndieFeedback

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

IndieFeedback

A zero-cost, GitHub-powered feedback system for iOS and macOS apps. Collect bug reports and feature requests from your users without any monthly subscription fees.

Why IndieFeedback?

  • Zero Cost - Uses GitHub Issues for storage and Cloudflare Workers free tier
  • No Database Required - GitHub is your database
  • Privacy Friendly - No user accounts needed, anonymous feedback
  • Full Control - Your data stays in your GitHub repository
  • Cross-Platform - Works on iOS 17+ and macOS 14+

See It in Action

IndieFeedback demo cycling through the feedback board, the one-box composer with duplicate detection, the success screen, and the smart prompt

Feedback board Duplicate detection Success Smart prompt
Feedback board with voting Composer suggesting an existing item to vote on instead Success screen showing the created item Smart feedback prompt

Every screen is native SwiftUI. The composer derives the title from the first line, auto-detects bug vs. idea, and surfaces similar existing feedback while typing — so users vote instead of filing duplicates.

How It Works

┌─────────────────┐     HTTPS      ┌─────────────────────┐     GitHub API     ┌─────────────────┐
│  iOS/macOS App  │ ──────────────▶│  Cloudflare Worker  │ ──────────────────▶│  GitHub Issues  │
│                 │◀────────────── │    (Edge Proxy)     │◀────────────────── │   (Storage)     │
└─────────────────┘                └─────────────────────┘                    └─────────────────┘
  1. App sends feedback to your Cloudflare Worker
  2. Worker proxies requests to GitHub Issues API
  3. GitHub stores feedback as issues with labels
  4. Votes live in a bot-maintained ledger comment on each issue (anonymized device hashes) — GitHub is the source of truth, Cloudflare KV caches counts for speed
  5. One worker can serve your whole portfolio — map multiple apps to repos and route with an appID

Quick Start

Step 1: Deploy the Cloudflare Worker

  1. Create a Cloudflare account (free)

  2. Install Wrangler CLI:

    npm install -g wrangler
    wrangler login
  3. Navigate to the worker directory:

    cd Sources/Worker
  4. Create your config from the template and update it with your settings:

    cp wrangler.toml.example wrangler.toml
    name = "your-app-feedback"
    account_id = "your-cloudflare-account-id"
    
    [vars]
    REPO_OWNER = "your-github-username"
    REPO_NAME = "your-repo-name"
  5. Set up GitHub credentials — two options:

    GitHub App (recommended): never needs renewal, scoped to just your feedback repos. Create an App with Issues read/write permission, install it on your repo, then:

    wrangler secret put GITHUB_APP_ID
    wrangler secret put GITHUB_APP_PRIVATE_KEY < app-pkcs8.pem

    See Sources/Worker/SETUP.md for the full walkthrough (including the one-line key conversion).

    Personal Access Token (simpler): create a classic token with repo scope, then:

    wrangler secret put GITHUB_TOKEN
    # Paste your token when prompted
  6. Create the KV namespace used for caching, rate limiting, and vote tracking:

    wrangler kv namespace create FEEDBACK_CACHE
    wrangler kv namespace create FEEDBACK_CACHE --preview

    Copy the output IDs into your wrangler.toml

  7. Deploy:

    wrangler deploy

    You'll get a URL like: https://your-app-feedback.your-subdomain.workers.dev

Step 2: Add to Your App

Using Swift Package Manager

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/rshankras/IndieFeedback", from: "1.2.0")
]

Or in Xcode: File → Add Package Dependencies → Enter the repository URL

Basic Usage

import IndieFeedback

struct ContentView: View {
    var body: some View {
        IndieFeedbackView(
            workerURL: "https://your-app-feedback.workers.dev"
        )
    }
}

In a Sheet

struct MyView: View {
    @State private var showFeedback = false

    var body: some View {
        Button("Send Feedback") {
            showFeedback = true
        }
        .sheet(isPresented: $showFeedback) {
            IndieFeedbackView(
                workerURL: "https://your-app-feedback.workers.dev",
                appName: "MyApp"
            )
        }
    }
}

Integration Points at a Glance

Every entry point is one line, and they all accept the same optional appID:/apiSecret: parameters:

Entry point One-liner Best for
Full feedback board IndieFeedbackView(workerURL:) A sheet, tab, or navigation destination
Shake gesture / ⌘⇧F .shakeToFeedback(workerURL:) Ambient, app-wide capture
Floating button .feedbackButton(workerURL:) Betas & feedback-forward apps
Settings row FeedbackSettingsRow(workerURL:) Where users look first
macOS Help menu FeedbackCommands() Every Mac app (it's the convention)
Smart prompt .feedbackPrompt(workerURL:trigger:) Proactively asking at the right moment
Programmatic FeedbackCenter.present() + .feedbackSheet(workerURL:) Custom buttons, error screens, onboarding
Shipped toast .feedbackShippedToast(workerURL:) Closing the loop when voted items ship

Recommended recipes:

  • Minimal (utility app): FeedbackSettingsRow + .shakeToFeedback
  • Standard (most apps): those two + .feedbackShippedToast + .feedbackPrompt(trigger: .afterEvents(...))
  • Feedback-forward (beta, building in public): add .feedbackButton and surface the full board in your menu
  • macOS: always add FeedbackCommands() — two lines, matches every Mac app users know

Don't stack every capture entry point at once — a Settings row, one ambient gesture, and a well-timed prompt covers discovery without feeling naggy.

Increasing Discoverability

Users often don't notice feedback options buried in settings. IndieFeedback provides optional components to make feedback more discoverable:

Shake to Feedback

On iOS, users can shake their device to instantly open the feedback form. On macOS, the keyboard shortcut Cmd+Shift+F does the same.

ContentView()
    .shakeToFeedback(workerURL: "https://your-worker.workers.dev")

Tip: Mention this during onboarding - "Shake anytime to send feedback!"

Floating Feedback Button

A persistent button that floats over your content, always visible and accessible.

// As a view modifier (easiest)
TabView { ... }
    .feedbackButton(
        workerURL: "https://your-worker.workers.dev",
        alignment: .bottomTrailing
    )

// Or as a standalone view
ZStack(alignment: .bottomTrailing) {
    MainContent()
    FeedbackButton(
        workerURL: "https://your-worker.workers.dev",
        style: .labeled("Feedback"),
        tint: .blue
    )
    .padding()
}

Button styles:

  • .compact - Icon only
  • .expanded - Expands on hover (macOS)
  • .labeled("Text") - Icon with custom text

Help Menu (macOS) and Settings Row

Meet users where they already look. On the Mac, feedback belongs in the Help menu:

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .feedbackSheet(workerURL: "https://your-worker.workers.dev")
        }
        .commands {
            FeedbackCommands()   // Help → Send Feedback…
        }
    }
}

And in your Settings screen, drop in a ready-made row that opens the full feedback board:

Section("Support") {
    FeedbackSettingsRow(workerURL: "https://your-worker.workers.dev")
}

If you instrument feedback opens, the row takes an optional onPresent hook:

FeedbackSettingsRow(workerURL: "https://your-worker.workers.dev") {
    Analytics.logEvent("feedback_opened", parameters: ["source": "settings"])
}

You can also trigger the composer programmatically from anywhere with FeedbackCenter.present().

Smart Feedback Prompt

A non-intrusive prompt that appears after certain conditions are met.

ContentView()
    .feedbackPrompt(
        workerURL: "https://your-worker.workers.dev",
        trigger: .afterSessions(5),      // Show after 5 app opens
        cooldown: .days(30)              // Don't ask again for 30 days
    )

Trigger options:

  • .afterEvents("export-done", count: 3) - Recommended: right after a positive moment (record them with FeedbackEvents.record("export-done")) — asking after the user succeeded converts far better than asking at launch
  • .afterSessions(5) - After N app sessions
  • .afterDays(7) - After N days since install
  • .afterSessionsAndDays(sessions: 3, days: 7) - Both conditions
  • .immediately - For testing

"You Asked, We Built It" Toast

The retention loop no hosted service can match: because votes are tracked per device, the app can detect when something this user voted for ships and celebrate it:

ContentView()
    .feedbackShippedToast(workerURL: "https://your-worker.workers.dev")

When feedback the user voted on moves to completed or released, they get a one-time "🎉 You asked, we built it!" toast with a button to see what's new. Checks run at most every 6 hours and never celebrate items that shipped before the user first saw them.

Customize the message:

.feedbackPrompt(
    workerURL: "https://your-worker.workers.dev",
    trigger: .afterSessions(5),
    cooldown: .weeks(4),
    title: "Enjoying MyApp?",
    message: "We'd love to hear what you think!",
    positiveButton: "Share Feedback",
    negativeButton: "Not Now"
)

Features

For Users

  • One-box composer: type a thought, the title and type (bug/idea) are derived automatically — no form schema
  • Duplicate detection while typing: similar existing feedback appears with a "vote instead" action
  • Vote on existing feedback, with a success screen that shows the created item (not a dead-end alert)
  • View feedback status (Open, Planned, In Progress, Completed, Released)
  • A "You asked, we built it" toast when something they voted for ships
  • Add comments to feedback items
  • Search and filter feedback

For Developers

  • All feedback appears as GitHub Issues
  • Use GitHub labels for organization
  • Manage status via GitHub labels
  • Reply to users through GitHub comments
  • View voting data in issue comments

GitHub Labels

IndieFeedback uses these labels to organize feedback:

Label Purpose
user-feedback All feedback items
bug Bug reports
enhancement Feature requests
status:planned Marked for implementation
status:in-progress Currently being worked on
status:completed Implementation complete
status:released Available in a release

Create these labels in your GitHub repository for proper categorization.

API Endpoints

Endpoint Method Description
/health GET Health check
/feedback/list GET List all feedback
/feedback/search GET Search feedback (duplicate detection)
/feedback/submit POST Submit new feedback
/feedback/:id GET Get feedback details
/feedback/:id/vote POST Toggle vote
/feedback/:id/comment POST Add comment
/roadmap GET Get milestones
/changelog GET Get releases

How It Compares

IndieFeedback Canny / Sleekplan WishKit Fider (self-hosted)
Cost $0 (free tiers) Free tier, then $/mo Free tier, then $/mo ~$5+/mo VPS
Native SwiftUI UI Web embed Web
Where feedback lives Your GitHub Issues Their database Their database Your database
Infrastructure you run One Cloudflare Worker None None Server + DB
Reply to users from GitHub (incl. mobile app) Their dashboard Their dashboard Their web UI

The trade-off is honest: setup takes ~30 minutes instead of five, and it's Apple-platforms-only. In exchange you own every byte, pay nothing, and triage feedback with the GitHub workflow you already use.

Example Apps

The repository includes example apps demonstrating all components — both consume the actual package via a local path dependency, so they always exercise the real API:

  • IOSExample/ - iOS app with shake gesture, floating button, and smart prompt
  • MacOSExample/ - macOS app with Cmd+Shift+F shortcut, expandable button, and smart prompt

Both examples show how to combine multiple components together. Point workerURL at your deployed worker and run them to see the features in action (opening the example projects requires Xcode 16+).

One Worker, Many Apps

If you ship several apps, you don't need a worker deployment per app. Map app IDs to repos in wrangler.toml:

APPS = '{"myapp": {"owner": "you", "repo": "MyApp"}, "otherapp": {"owner": "you", "repo": "OtherApp"}}'

Then pass the ID from each app:

IndieFeedbackView(
    workerURL: "https://feedback.your-subdomain.workers.dev",
    appID: "myapp"
)

Adding a new app to your portfolio becomes a config line plus a redeploy. Requests without an appID fall back to the worker's REPO_OWNER/REPO_NAME, so existing single-app setups keep working unchanged.

Configuration Options

IndieFeedbackView(
    workerURL: String,   // Required: Your Cloudflare Worker URL
    appName: String?,    // Optional: App name for Keychain storage
    appID: String?,      // Optional: app ID for multi-tenant workers (?app=)
    apiSecret: String?   // Optional: shared secret (X-API-Secret header)
)

Costs

  • GitHub: Free (public or private repos)
  • Cloudflare Workers: Free tier includes 100,000 requests/day
  • Cloudflare KV: Free tier includes 100,000 reads/day, 1,000 writes/day

For most indie apps, you'll never exceed the free tiers.

Requirements

  • iOS 17.0+ / macOS 14.0+
  • Swift 5.9+
  • Xcode 15.0+

Privacy

  • No user accounts required
  • Device ID stored locally in Keychain (for vote tracking only)
  • Votes are recorded in GitHub as SHA-256 hashes of the device ID — nothing identifying appears in your repo
  • No personal data collected
  • All data stored in your own GitHub repository

Troubleshooting

Worker returns 401 Unauthorized

  • PAT mode: verify your GitHub token has repo scope and is set (wrangler secret list)
  • GitHub App mode: verify the App is installed on the target repo, and that the private key was converted to PKCS#8 (see Sources/Worker/SETUP.md)
  • If the response says Unauthorized with no GitHub error, your worker has API_SECRET set — pass the same value as apiSecret in the Swift client

Votes not persisting

  • Verify KV namespaces are created and bound in wrangler.toml
  • Check KV namespace IDs match

No feedback appearing

  • Confirm the worker URL is correct (no trailing slash)
  • Check GitHub repo has the required labels created
  • Verify network connectivity

License

MIT License - See LICENSE file for details.

Author

Created by Ravi Shankar

About

Zero-cost, GitHub-powered user feedback for iOS and macOS apps — a SwiftUI package + Cloudflare Worker with GitHub Issues as the database. Feature voting, roadmap, changelog; no accounts, no monthly fees.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors