Launchday Docs
Everything you need to ship and sell native apps: license management, release distribution, automatic update feeds, waitlists, file hosting, and a CLI to wire it all together.
What is Launchday?
Launchday is a backend platform for app developers. It handles the operational infrastructure every paid app needs, so you can focus on building, not plumbing.
License management
Generate, activate, and revoke license keys. Ed25519-signed payloads let your app verify licenses offline after the first check.
Release distribution
Upload builds for any platform. Signed download URLs served from your own S3-compatible storage, with a Sparkle appcast for macOS auto-updates.
Waitlist
Collect email signups before launch. Assign beta access from the dashboard and send license keys automatically when a spot opens up.
CLI
The launchday CLI turns shipping a release into a one-liner. Upload, list, yank, and run a health check from your terminal.
Core concepts
- Product
- The top-level entity: your app. A product can have multiple platforms (macOS, iOS, Windows, etc.), each with its own releases and namespace.
- Platform & namespace
- Each platform gets a namespace, a short slug like
com.example.myapp. Namespaces are unique per platform, so you can reuse the same identifier across macOS and Windows. The namespace drives the download and appcast URLs. - Configurations
- Email and storage configs are org-level resources you create once and attach to any product or platform. This lets you share a single Mailgun domain or S3 bucket across all your products.
- Signed payloads
- License validation responses are signed with a per-product Ed25519 key. Your app verifies the signature locally with no network call needed after the first activation.
Getting started
1. Create a product
In the dashboard, create a product and give it a name. Then add at least one platform (e.g. macOS) with a namespace. The namespace is a reverse-domain identifier like com.example.myapp.
2. Configure your API endpoint
By default, all API calls go directly to api.launchday.one — no DNS setup required. Pass your namespace and platform in the request body to identify your product. This is the standard integration path and requires no domain configuration.
Optional (Studio plan): add a CNAME to serve the license API from your own subdomain. When a custom domain is set, the namespace and platform fields are no longer required — the host header identifies your product automatically.
No backend needed? If you sell through Stripe or Paddle, Launchday's Payment Webhooks feature can generate and email a license to your customer automatically when a payment completes — no server-side code required on your end.
launchday.yourapp.com CNAME api.launchday.one // license API (Studio plan)
If using a custom domain, set it on your platform in Products → your product → Platforms, then add the domain in your Fly dashboard or contact support to provision the TLS certificate.
3. Get your public key
Go to Products → your product → Settings → Public Key and copy the hex-encoded Ed25519 public key. Bundle it with your app. It never changes unless you rotate keys.
4. Attach configurations
Create an email config and a storage config under Settings, then link them to your product. Without an email config, license emails won't be sent. Without a storage config, release uploads will use the default Launchday storage.
License
Integration
Launchday uses Ed25519-signed payloads so your app can verify licenses completely offline after the first network check. The flow is:
-
1
On first launch, call
POST /v1/license/validatewithmode: "activate"to bind the device -
2
Verify the returned
signatureagainst thepayloadusing your bundled Ed25519 public key -
3
Cache the
payload+signaturein the Keychain for offline use -
4
On subsequent launches, use
mode: "check". If offline, fall back to the cached payload (reject if expired)
Recommended launch flow
App launch
│
├─ Load cached payload from Keychain
│ │
│ ├─ Valid & not expired → allow (offline ok)
│ └─ Missing / expired → must validate online
│
└─ POST /v1/license/validate (mode: "activate" first time, "check" after)
│
├─ valid: true → verify signature → cache payload → allow
└─ valid: false → show license entry UI
https://api.launchday.one (or your custom domain)
Downloads & appcast: https://releases.launchday.one
Content-Type: application/json
Validation
Activate a license on a device for the first time, or check an already-activated license.
POST /v1/license/validate
Request body
{
"license_key": "MYAPP-XXXX-XXXX-XXXX",
"device_id": "unique-device-identifier",
"mode": "activate",
"namespace": "com.example.myapp", // required by default; omit only with a Studio plan custom domain
"platform": "macos" // required by default; omit only with a Studio plan custom domain
}
| Field | Type | Required | Description |
|---|---|---|---|
license_key |
string | ✓ | The license key to validate |
device_id |
string | ✓ | A stable unique ID for this device. Use IOPlatformUUID on macOS, identifierForVendor on iOS. |
mode |
string | ✓ |
"activate": binds device if not already bound.
"check": verifies an already-bound device without creating a new binding.
|
namespace |
string | ✓ default | Your platform namespace (e.g. com.example.myapp). Required when calling api.launchday.one directly — this is the standard path and requires no DNS setup. Only omit if using a custom CNAME domain (Studio plan). |
platform |
string | ✓ default | Platform identifier. One of macos, ios, windows, linux, android. Required alongside namespace when using the default api.launchday.one endpoint. |
Success response
{
"valid": true,
"license_status": "active",
"key_id": "abc123",
"signature": "a1b2c3...hex",
"payload": {
"license_key": "MYAPP-XXXX-XXXX-XXXX",
"device_id": "unique-device-identifier",
"namespace": "com.example.myapp",
"license_status": "active",
"issued_at": "2026-02-27T12:00:00Z",
"expires_at": "2027-02-27T12:00:00Z"
}
}
Error responses
// 200 OK (invalid license) or 4xx
{ "error": "license_not_found" }
{ "error": "revoked" }
{ "error": "expired" }
{ "error": "max_devices_reached" }
{ "error": "device_not_bound" }
{ "error": "invalid_mode" }
Deactivation
Remove a device binding. Call this when the user uninstalls or transfers their license.
POST /v1/license/deactivate
Request body
{
"license_key": "MYAPP-XXXX-XXXX-XXXX",
"device_id": "unique-device-identifier"
}
Success response
{ "deactivated": true }
Request by email
Creates a new license (or resends an existing one) and emails it to the user. Requires an email config linked to the product.
POST /v1/license/request
Request body
{
"email": "user@example.com"
}
Success response
{
"success": true,
"message": "If an account exists, a license key has been sent."
}
Offline verification
Every successful validate response includes a payload and signature. Verify them locally using your bundled Ed25519 public key.
-
1
JSON-encode the
payloadobject. Field order is alphabetical (Go'sjson.Marshal):device_id,expires_at,issued_at,license_key,license_status,namespace -
2
Verify the hex-encoded
signatureagainst the encoded bytes using Ed25519 -
3
Check
expires_atis in the future andlicense_statusis"active"
Swift example
import CryptoKit
func verifyPayload(
_ response: ValidationResponse,
publicKeyHex: String
) -> Bool {
guard
let pubKeyData = Data(hexString: publicKeyHex),
let pubKey = try? Curve25519.Signing
.PublicKey(rawRepresentation: pubKeyData),
let sigData = Data(hexString: response.signature),
let payloadData = try? JSONEncoder()
.encode(response.payload) // must match Go field order
else { return false }
guard pubKey.isValidSignature(
sigData, for: payloadData
) else { return false }
let expires = response.payload.expiresAt
return expires > Date() &&
response.payload.licenseStatus == "active"
}
Waitlist
Collect email signups before your app is ready. The waitlist API is public. Embed a simple form on your landing page. From the dashboard you can view the queue, assign beta spots, and trigger automatic invite emails.
When you assign a spot, Launchday generates a license key and sends it via your configured email config. No code required on your end.
Join waitlist
Public endpoint. No authentication required.
POST /v1/waitlist/join
Request body
{
"email": "user@example.com",
"product_id": "uuid-of-product",
"form_token": "hmac-hex-string",
"cf_turnstile_response": "turnstile-token" // optional
}
No CNAME required — requests go directly to https://api.launchday.one/v1/waitlist/join. Set a Waitlist Domain on your platform to restrict submissions to requests originating from that domain.
Success response
{ "success": true }
Payment Webhooks
Launchday can automatically generate and email a license to your customer when a payment completes — no backend required on your side. Connect your payment provider once, and Launchday handles the rest.
Supported providers and events:
-
Stripe —
checkout.session.completed -
Paddle Billing —
transaction.completed
Webhook endpoint
POST https://api.launchday.one/v1/webhooks/{webhook_id}
The webhook_id is an opaque UUID generated by Launchday — not guessable, and rotatable at any time from the dashboard.
Setup
- 1 In your product dashboard, go to Settings → Payments
- 2 Click Add webhook — choose your payment provider (Stripe or Paddle) and the scope (all platforms or a specific platform)
- 3 Copy the generated webhook URL from Launchday
- 4 Paste it as a webhook endpoint in your Stripe dashboard or Paddle dashboard
- 5 Copy the signing secret generated by Stripe or Paddle and paste it back into Launchday on the same webhook settings screen
Scope behaviour
| Scope | Result |
|---|---|
| Product-scoped | One license per platform is issued. If your product has macOS and iOS platforms, the customer receives a license for each. |
| Platform-scoped | One license for just the selected platform. |
Configuration
Configurations are org-level resources. Create them once and attach them to any product or platform. A platform-level config takes precedence over a product-level one.
Email: Mailgun
Launchday uses Mailgun to deliver license keys and waitlist notifications. Additional providers (SendGrid, Postmark, etc.) may be supported in future.
Setup
-
1
In your Mailgun dashboard, add and verify your sending domain (e.g.
mail.yourapp.com) - 2 Generate a sending API key (not the account key) in Mailgun → API Keys
- 3 In the Launchday dashboard, go to Settings → Email configs → New and fill in the fields below
- 4 Link the config to your product under Products → your product → Settings
Fields
| Field | Required | Description |
|---|---|---|
mailgun_api_key |
✓ | Your Mailgun sending API key. Stored encrypted at rest. |
mailgun_domain |
✓ | Your verified Mailgun sending domain (e.g. mail.yourapp.com) |
email_sender |
— | From address shown to recipients (e.g. Your App <hello@yourapp.com>) |
email_reply_to |
— | Reply-to address for support replies |
template_license_issued_html |
— | HTML email body for license delivery. Use {{.LicenseKey}}, {{.Email}}, {{.AppName}} as template variables. |
template_license_issued_text |
— | Plain-text fallback for the license email. Same template variables apply. |
Storage
Launchday provides managed storage for your releases by default. You can also bring your own S3-compatible bucket — AWS S3, Backblaze B2, MinIO, and any other S3-compatible provider are supported.
Setup
- 1 Create a bucket in your storage provider and note the bucket name, region, and endpoint (if applicable)
- 2 Generate an access key/secret with read+write permissions on the bucket
- 3 Go to Settings → Storage configs → New in the dashboard and fill in the fields below
- 4 Link the config to your product under Products → your product → Settings
Fields
| Field | Required | Description |
|---|---|---|
provider |
✓ | s3 for AWS S3 or any S3-compatible provider |
bucket |
✓ | Bucket name |
region |
✓ | AWS region (e.g. us-east-1) or the provider's equivalent |
endpoint_url |
— | Custom endpoint for non-AWS providers (e.g. Cloudflare R2: https://<account-id>.r2.cloudflarestorage.com). Leave blank for AWS S3. |
path_prefix |
— | Optional key prefix for all uploads (e.g. launchday/) |
credentials |
✓ | JSON object with access_key_id and secret_access_key. Stored encrypted at rest. |
CLI
The launchday CLI turns shipping a release into a single command. Install it, run launchday init once, then use launchday build or launchday upload to publish.