Skip to content
5 min read Guides Vapor

Introducing the FeedbackKit Vapor SDK

Today we are introducing Vapor SDK 1.0, a Swift package that lets a server built with Vapor 4 talk to FeedbackKit. The app SDKs put a request board in front of your users. This one is for the code behind them: your backend can file a request, list the board for a web page of your own, record a vote or a comment, tell FeedbackKit what each user pays, and, with an account token, manage projects, members and statuses.

The short version

  • One configure call in configure.swift, then req.feedbackKit in any route.
  • Project-key operations: list, get and file requests, vote and unvote, list and add comments, and register a user with their MRR.
  • Account-token operations through req.feedbackKitAdmin(token:): projects, members and invites, status changes and merges, and the ticket integrations.
  • Errors are a FeedbackKitError that conforms to Vapor's AbortError, so a failure thrown from a route becomes the matching HTTP status.
  • It needs Swift 6.2 and Vapor 4.89 or later. 1.0.0 has no git tag, so you pin it by commit, as shown below.

What is in Vapor SDK 1.0

One configure call

app.feedbackKit.configure(apiKey:environment:) stores your project's API key and the server to talk to. After that, req.feedbackKit gives you the project-key client inside a route, built on the request's own HTTP client and logger. Outside a request, in a lifecycle handler or a scheduled job, app.feedbackKit.client() returns the same client. Either one throws FeedbackKitError.notConfigured if the configure call has not run.

Project-key operations

These send your project's key in the X-API-Key header, the same key the app SDKs use:

  • feedback.list(status:category:includeMerged:userId:), feedback.get(id:userId:) and feedback.create(_:). Pass a userId to a read and each request's hasVoted answers for that user.
  • votes.vote(feedbackId:request:) and votes.unvote(feedbackId:request:). A CreateVoteRequest carries the user id, and can carry an email and a notifyStatusChange opt-in.
  • comments.list(feedbackId:) and comments.create(feedbackId:request:).
  • users.register(_:), which records a user id with an optional MRR, so your team can rank requests by the revenue behind them.

Responses are typed: a FeedbackResponse has the status, category, vote and comment counts, the total MRR behind the request, and the link to any ticket it was sent to.

Account-token operations

req.feedbackKitAdmin(token:) returns a client that sends a Bearer token for a FeedbackKit account instead of a project key. auth.login(_:) returns such a token. With it, your server can do what the account could do in the admin app:

  • Projects: list, create, update, archive and unarchive, regenerate the API key, transfer ownership, and set the Slack settings and the statuses a project uses.
  • Members: list, add, change a role, remove, and manage invites.
  • Requests: feedback.update(id:_:) changes a status, title, category or rejection reason, and feedback.merge(_:) folds duplicates into one request.
  • Integrations: settings, single and bulk ticket creation for GitHub, ClickUp, Notion, Monday.com, Linear, Trello, Airtable, Asana and Basecamp, plus the Email Campaign settings.

The server applies the same rules as everywhere else: an account's role and plan decide what each call may do, and a call past a plan limit answers with FeedbackKitError.paymentRequired.

Errors

FeedbackKitError has a case per answer: badRequest, unauthorized, invalidApiKey, paymentRequired, forbidden, notFound, conflict (for example a repeat vote), serverError and decodingError. Because it conforms to AbortError, a route can let it propagate and Vapor answers with the same status code and a readable reason.

Installing 1.0

1.0.0 has no git tag. The package lives in the SwiftlyFeedbackKit-Vapor-SDK repository, and the 1.0.0 code is its commit e2be1ee, from 14 February. The earliest tag in that repository is 1.1.0, so Swift Package Manager cannot resolve 1.0.0 as a version. To install 1.0 itself, pin the commit. Add it next to your Vapor dependency in Package.swift:

.package(
    url: "https://github.com/Swiftly-Developed/SwiftlyFeedbackKit-Vapor-SDK.git",
    revision: "e2be1ee98fef47e3d0f88745084d74e9d11fad0f"
),

and add the product to your app target's dependencies:

.product(name: "SwiftlyFeedbackKitVapor", package: "SwiftlyFeedbackKit-Vapor-SDK"),

The package's manifest uses Swift tools 6.2 and sets macOS 13 as its minimum Apple platform.

Getting started

Keep the key in an environment variable and configure once:

import Vapor
import SwiftlyFeedbackKitVapor

public func configure(_ app: Application) async throws {
    app.feedbackKit.configure(
        apiKey: Environment.get("FEEDBACKKIT_API_KEY") ?? "",
        environment: .custom(URL(string: "https://api.feedbackkit.prod.swiftly-developed.com")!)
    )
    try routes(app)
}

Which address. In 1.0.0, .production points at getfeedbackkit.com, the website's address. The FeedbackKit server your projects live on answers at api.feedbackkit.prod.swiftly-developed.com, the address the Swift SDK uses, so pass that with .custom. The package adds /api/v1 itself. .local is for running a FeedbackKit server on your own machine.

Then use it from a route. Trailmate, a fictional hiking app, lets people suggest a feature from its website, and its server lists the approved requests and files new ones under the visitor's account id:

struct NewIdea: Content {
    let title: String
    let description: String
    let accountID: String
}

func routes(_ app: Application) throws {
    app.get("ideas") { req async throws -> [FeedbackResponse] in
        try await req.feedbackKit.feedback.list(status: .approved)
    }

    app.post("ideas") { req async throws -> FeedbackResponse in
        let idea = try req.content.decode(NewIdea.self)
        return try await req.feedbackKit.feedback.create(CreateFeedbackRequest(
            title: idea.title,
            description: idea.description,
            category: .featureRequest,
            userId: idea.accountID
        ))
    }
}

To weigh votes by revenue, register what each account pays when their subscription changes:

_ = try await req.feedbackKit.users.register(
    RegisterSDKUserRequest(userId: account.id, mrr: 4.99)
)

Use the same user id your app passes to its FeedbackKit SDK, so a person's votes and requests from the app and from your server count as one user. The reference for every call is in the Vapor SDK docs.

How FeedbackKit fits in

Requests your server files land on the same board as the ones filed in your apps. Your team reads them in the FeedbackKit admin app for iPhone, iPad and Mac, or in the web admin, merges duplicates, replies, sorts by votes or by the MRR behind them, and moves a request through Approved, In Progress and TestFlight to Completed. A submitter who left an email is told by email when the status changes, and the team gets a push notification for new requests and comments. See pricing for what each plan includes.

Here is how triage looks in FeedbackKit, with sample data from Trailmate:

Requests your team sorts

Requests filed from your app or your server land on one board, where your team sets their status.

The other SDKs

For the app side, the Swift SDK puts the board in iPhone, iPad, Mac and Vision Pro apps, and the Kotlin SDK does the same on Android.

Every plan includes the Vapor SDK. Start free, create a project, and connect your server to it.

Keep reading

Collect feedback in your own app

FeedbackKit ships SDKs for Swift, Kotlin, React Native, Flutter and JavaScript, and syncs what your users ask for into the tracker your team already uses.