diff --git a/README.md b/README.md
index 66e1346..32e1a30 100644
--- a/README.md
+++ b/README.md
@@ -51,11 +51,11 @@ Every link expires. Every file is deleted. By design.
| Tier | File size | Retention | Uploads | Sync |
| ---- | --------- | --------- | ------- | ---- |
-| Free | 100 MB per file | Up to 24 hours | 3 per day | Local history |
+| Free launch access | 2 GB per file | Up to 30 days | Unlimited | Local history |
| Pro | 2 GB per file | Up to 30 days | Unlimited | iCloud metadata sync |
-The backend is the source of truth for caps and retention. Beta allowances are
-controlled by a secure allowlist, never by a global "free equals pro" default.
+The backend is the source of truth for caps and retention. For launch, Free
+uses Pro upload ceilings while keeping account-level sync features paid.
## How it works
diff --git a/apple/FastSharedApp/Scenes/PaywallView.swift b/apple/FastSharedApp/Scenes/PaywallView.swift
index be629ee..4f0e64c 100644
--- a/apple/FastSharedApp/Scenes/PaywallView.swift
+++ b/apple/FastSharedApp/Scenes/PaywallView.swift
@@ -215,7 +215,7 @@ struct PaywallView: View {
case .cloudSyncRequested:
return "Pro mirrors your link history to your private iCloud — no extra setup."
case .longRetentionRequested:
- return "Pro supports up to 30-day retention. Free tops out at 24 hours."
+ return "Launch access supports up to 30-day retention. Pro adds sync, support, and Lifetime Family Sharing."
case .largeFileRequested:
return "Pro raises the cap to 2 GB per file and keeps the same one-gesture flow."
case .serverForced:
diff --git a/apple/FastSharedApp/Screenshot/AppStoreScreenshotMode.swift b/apple/FastSharedApp/Screenshot/AppStoreScreenshotMode.swift
index de0eda0..5ce88e8 100644
--- a/apple/FastSharedApp/Screenshot/AppStoreScreenshotMode.swift
+++ b/apple/FastSharedApp/Screenshot/AppStoreScreenshotMode.swift
@@ -28,7 +28,7 @@ enum AppStoreScreenshotScene: String, CaseIterable {
case .history:
return "Keep control after you send."
case .pro:
- return "Go Pro when you need more room."
+ return "Go Pro when you want sync."
}
}
@@ -37,13 +37,13 @@ enum AppStoreScreenshotScene: String, CaseIterable {
case .shareFlow:
return "FastShared turns a file into a temporary short link and copies it automatically."
case .retention:
- return "Default 24-hour links keep cleanup automatic. Pro unlocks up to 30 days."
+ return "Default 24-hour links keep cleanup automatic. Launch access supports up to 30 days."
case .progress:
return "Large files continue cleanly with visible progress and a ready-to-paste link."
case .history:
return "Find recent links, copy again, share, or revoke before expiration."
case .pro:
- return "Unlimited daily uploads, 2 GB files, longer retention, and iCloud sync."
+ return "Sync link history across devices, get priority support, and keep Lifetime in the family."
}
}
}
diff --git a/apple/Packages/FastSharedCore/Sources/FastSharedCore/Subscription/TierCaps.swift b/apple/Packages/FastSharedCore/Sources/FastSharedCore/Subscription/TierCaps.swift
index 989487d..517fbd1 100644
--- a/apple/Packages/FastSharedCore/Sources/FastSharedCore/Subscription/TierCaps.swift
+++ b/apple/Packages/FastSharedCore/Sources/FastSharedCore/Subscription/TierCaps.swift
@@ -19,10 +19,12 @@ public struct TierCaps: Sendable, Codable, Equatable {
self.allowsCloudSync = allowsCloudSync
}
+ /// Launch Free defaults — unlimited uploads, 2 GB per file, 30 days retention.
+ /// iCloud sync stays Pro-only so paid entitlements still map to account-level features.
public static let free = TierCaps(
- dailyUploadLimit: 3,
- maxFileSizeBytes: 100 * 1024 * 1024,
- maxRetentionSeconds: 86_400,
+ dailyUploadLimit: nil,
+ maxFileSizeBytes: 2 * 1024 * 1024 * 1024,
+ maxRetentionSeconds: 2_592_000,
allowsCloudSync: false
)
diff --git a/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UploadServiceTests.swift b/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UploadServiceTests.swift
index 39394e6..8eefa4d 100644
--- a/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UploadServiceTests.swift
+++ b/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UploadServiceTests.swift
@@ -220,7 +220,12 @@ final class UploadServiceTests: XCTestCase {
background: background,
orchestrator: orchestrator,
usageTracker: tracker,
- subscriptionStore: nil) // nil → Free fallback
+ subscriptionStore: CustomCapSubscriptionStore(caps: TierCaps(
+ dailyUploadLimit: 3,
+ maxFileSizeBytes: 2 * 1024 * 1024 * 1024,
+ maxRetentionSeconds: 30 * 86_400,
+ allowsCloudSync: false
+ )))
let fileURL = try Self.writeTempFile(bytes: 32)
do {
@@ -238,8 +243,8 @@ final class UploadServiceTests: XCTestCase {
}
func test_enqueue_onFileTooLargeForFree_throwsSubscriptionGate() async throws {
- // 100 MB cap + 1-byte file > cap (using small file + lowered cap via
- // a custom FreeCapsSubscriptionStore).
+ // Use a tiny custom cap so a 64-byte fixture blows it without creating
+ // a huge test file.
let tracker = UsageTracker(clock: UsageTrackerTests.FakeClock("2026-04-19"),
defaults: isolatedDefaults())
let store = SwiftDataStore.inMemoryForTests()
@@ -353,8 +358,8 @@ final class UploadServiceTests: XCTestCase {
background: background,
orchestrator: orchestrator,
usageTracker: tracker,
- // nil subscriptionStore → falls back to .free caps but uses
- // the injected tracker so each test starts with a clean counter.
+ // Avoid quota gates in the shared happy-path helper; negative
+ // preflight coverage injects custom caps in focused tests.
subscriptionStore: AlwaysProSubscriptionStore())
return (service, mock, store, clipboard, orchestrator)
}
diff --git a/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UsageTrackerTests.swift b/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UsageTrackerTests.swift
index 84ece4d..08a9ba5 100644
--- a/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UsageTrackerTests.swift
+++ b/apple/Packages/FastSharedCore/Tests/FastSharedCoreTests/UsageTrackerTests.swift
@@ -67,12 +67,12 @@ final class UsageTrackerTests: XCTestCase {
XCTAssertEqual(newDay, 0)
}
- func test_remaining_forFree_cappedAt3() async {
+ func test_remaining_forLaunchFree_returnsUnlimitedSentinel() async {
let tracker = UsageTracker(clock: FakeClock("2026-04-19"), defaults: isolatedDefaults())
_ = await tracker.increment()
_ = await tracker.increment()
let remaining = await tracker.remaining(for: .free)
- XCTAssertEqual(remaining, 1)
+ XCTAssertEqual(remaining, UsageUnlimited)
}
func test_remaining_forPro_returnsUnlimitedSentinel() async {
diff --git a/apple/fastlane/metadata/en-US/description.txt b/apple/fastlane/metadata/en-US/description.txt
index 1e9e998..002e694 100644
--- a/apple/fastlane/metadata/en-US/description.txt
+++ b/apple/fastlane/metadata/en-US/description.txt
@@ -26,10 +26,12 @@ YOUR FILES, YOUR RULES
- Bearer links with high-entropy tokens. The link is the credential. Revoke any time.
- noindex, no-referrer, no-store on every resolve so tokens stay out of search engines, referrer chains, and caches.
-FASTSHARED PRO
+LAUNCH ACCESS
- Unlimited uploads per day.
- Files up to 2 GB.
- Link retention up to 30 days.
+
+FASTSHARED PRO
- iCloud history sync across iPhone, iPad, and Mac.
- Family Sharing on Lifetime.
- Priority support.
diff --git a/apple/fastlane/metadata/review_information/notes.txt b/apple/fastlane/metadata/review_information/notes.txt
index b4059ed..96ca7bf 100644
--- a/apple/fastlane/metadata/review_information/notes.txt
+++ b/apple/fastlane/metadata/review_information/notes.txt
@@ -12,6 +12,6 @@ To test Pro, use a sandbox Apple ID and these App Store Connect products:
- red.fastsha.pro.annual
- red.fastsha.pro.lifetime
-Pro unlocks unlimited uploads, 2 GB file size, 30-day retention, iCloud history sync, and priority support. Lifetime has Family Sharing enabled.
+For launch, the basic upload flow is intentionally unlimited: non-Pro users can upload without a daily cap, use files up to 2 GB, and choose retention up to 30 days. Pro remains available for iCloud history sync, priority support, and Lifetime Family Sharing.
User-generated content is limited to private bearer links created by the reviewer during testing. There is no public feed, user search, follower graph, chat, comments, or permanent hosting. Uploaded files are private, revocable, and automatically deleted after their retention window. The public report endpoint for abuse is /v1/report/:token.
diff --git a/backend/README.md b/backend/README.md
index 6e51d03..80454b7 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -254,9 +254,10 @@ Response:
}
```
-Each file in the batch counts as one event against the daily cap (free tier
-= 3/day) — a batch of 4 from a free user fails fast with `429` before any
-DB insert. Junction-table idempotency is keyed on `(share_link_id,
+Each file in the batch counts as one event when a daily cap is configured.
+Launch Free uses the unlimited sentinel, so the KV counter is skipped. If caps
+are tightened later, over-cap batches fail fast with `429` before any DB
+insert. Junction-table idempotency is keyed on `(share_link_id,
upload_job_id)`: re-completing the same `uploadId` is a no-op. Two slots
that dedup to the same R2 asset still occupy two distinct junction rows
(one per `uploadJobId`).
diff --git a/backend/src/lib/tierCaps.ts b/backend/src/lib/tierCaps.ts
index 2c78459..cc20e2f 100644
--- a/backend/src/lib/tierCaps.ts
+++ b/backend/src/lib/tierCaps.ts
@@ -1,9 +1,10 @@
// Single source of truth for per-tier quota limits. Both the free-tier
// enforcement middleware and `GET /v1/me` read these — no drift allowed.
//
-// Values are aligned with the public beta hotfix plan:
-// Free: 3 uploads/day, max 100 MB, TTL fixed at 24h, no Cloud Sync
-// Pro: unlimited (-1 sentinel) uploads, max 2 GB, TTL up to 30 days
+// Values are aligned with the launch plan:
+// Free: launch-unlimited uploads (-1 sentinel), max 2 GB, TTL up to 30 days,
+// no Cloud Sync
+// Pro: same upload caps, plus Cloud Sync
export interface TierCaps {
uploadsPerDay: number; // -1 means unlimited
@@ -32,9 +33,9 @@ export function toWireCaps(caps: TierCaps): TierCapsWire {
}
export const FREE_CAPS: TierCaps = {
- uploadsPerDay: 3,
- maxFileSizeMB: 100,
- maxRetentionHours: 24,
+ uploadsPerDay: -1,
+ maxFileSizeMB: 2048,
+ maxRetentionHours: 720,
allowsCloudSync: false,
};
diff --git a/backend/src/middleware/rateLimitFreeTier.ts b/backend/src/middleware/rateLimitFreeTier.ts
index 3814af4..65d128c 100644
--- a/backend/src/middleware/rateLimitFreeTier.ts
+++ b/backend/src/middleware/rateLimitFreeTier.ts
@@ -17,10 +17,11 @@ const UTC_DAY_SECONDS = 86_400;
// Free-tier enforcement. Runs AFTER auth() — requires `deviceId` in context.
// On Pro: returns immediately; caps live on the DB row, not KV.
-// On Free: enforces size cap (100 MB), daily-count cap (3/day), and silently
-// clamps retention policies that exceed 24h down to `oneDay`. Silent clamp
-// means the response includes `retentionClamped: true` but the request does
-// NOT 4xx — Free users get a reduced feature, not a rejection.
+// On Free: enforces the current Free caps from FREE_CAPS. During launch those
+// caps are intentionally generous (unlimited daily uploads, 2 GB files,
+// 30-day retention) while Cloud Sync remains Pro-only. If the caps are later
+// tightened, retention policies above the cap are silently clamped instead of
+// 4xxing the request.
//
// Body consumption is the big foot-gun here: Hono's `c.req.json()` consumes
// the underlying ReadableStream, so downstream Zod parsing would see an empty
diff --git a/backend/src/routes/uploads.ts b/backend/src/routes/uploads.ts
index 608fde1..19b4690 100644
--- a/backend/src/routes/uploads.ts
+++ b/backend/src/routes/uploads.ts
@@ -282,9 +282,9 @@ uploadRoutes.post('/batch', async (c) => {
}
}
- // Free-tier enforcement: each file in the bundle counts 1 against the daily
- // cap, plus the per-file size cap. Pro skips both. Mirrors the logic in
- // rateLimitFreeTier middleware but with count = items.length atomically.
+ // Free-tier enforcement mirrors rateLimitFreeTier middleware but reserves
+ // `items.length` atomically when a daily cap is configured. Launch Free caps
+ // use the unlimited sentinel, so this path skips the KV counter.
const enforce = await enforceBatchFreeTierLimits(c, db, deviceId, body);
if (enforce) return enforce;
diff --git a/backend/test/tierCaps.test.ts b/backend/test/tierCaps.test.ts
new file mode 100644
index 0000000..942978c
--- /dev/null
+++ b/backend/test/tierCaps.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, it } from 'vitest';
+import { FREE_CAPS, PRO_CAPS, toWireCaps } from '~/lib/tierCaps';
+
+describe('launch tier caps', () => {
+ it('keeps Free upload usage unlimited for launch without enabling Cloud Sync', () => {
+ expect(FREE_CAPS.uploadsPerDay).toBe(-1);
+ expect(FREE_CAPS.maxFileSizeMB).toBe(PRO_CAPS.maxFileSizeMB);
+ expect(FREE_CAPS.maxRetentionHours).toBe(PRO_CAPS.maxRetentionHours);
+ expect(FREE_CAPS.allowsCloudSync).toBe(false);
+
+ expect(toWireCaps(FREE_CAPS)).toEqual({
+ dailyUploadLimit: null,
+ maxFileSizeBytes: 2 * 1024 * 1024 * 1024,
+ maxRetentionSeconds: 30 * 24 * 60 * 60,
+ allowsCloudSync: false,
+ });
+ });
+});
diff --git a/web/src/components/FAQ.astro b/web/src/components/FAQ.astro
index 775f7f4..83c0698 100644
--- a/web/src/components/FAQ.astro
+++ b/web/src/components/FAQ.astro
@@ -7,7 +7,7 @@ interface QA {
const items: QA[] = [
{
q: 'How long do files live?',
- a: 'Free links can live up to 24 hours. Pro links can live up to 30 days. Every share has an expiry.',
+ a: 'Launch links can live up to 30 days. Every share has an expiry.',
},
{
q: 'Does the recipient need FastShared?',
diff --git a/web/src/components/Hero.astro b/web/src/components/Hero.astro
index d930382..c22b3f3 100644
--- a/web/src/components/Hero.astro
+++ b/web/src/components/Hero.astro
@@ -78,12 +78,12 @@
-
Free
-
100 MB · 24 h
+
Launch
+
2 GB · 30 d
Pro
-
2 GB · 30 d
+
iCloud sync
Storage
diff --git a/web/src/components/HowItWorks.astro b/web/src/components/HowItWorks.astro
index aac462b..14d8ebc 100644
--- a/web/src/components/HowItWorks.astro
+++ b/web/src/components/HowItWorks.astro
@@ -9,7 +9,7 @@ const steps = [
{
n: '02',
title: 'Set the deadline',
- caption: 'Default 24 hours. Pro can keep links alive up to 30 days.',
+ caption: 'Default 24 hours. Launch access can keep links alive up to 30 days.',
meta: 'Expiry is required',
},
{
diff --git a/web/src/pages/pricing.astro b/web/src/pages/pricing.astro
index 6727655..8a77326 100644
--- a/web/src/pages/pricing.astro
+++ b/web/src/pages/pricing.astro
@@ -6,7 +6,7 @@ import PlaneArc from '../components/PlaneArc.astro';
---
@@ -14,7 +14,7 @@ import PlaneArc from '../components/PlaneArc.astro';
'@context': 'https://schema.org',
'@type': 'Product',
name: 'FastShared Pro',
- description: 'Pro tier of FastShared — unlimited uploads, 30-day retention, iCloud sync, priority support. Available as Monthly, Annual, or one-time Lifetime.',
+ description: 'Pro tier of FastShared — iCloud sync, priority support, and Family Sharing on Lifetime. Available as Monthly, Annual, or one-time Lifetime.',
brand: { '@type': 'Brand', name: 'FastShared' },
offers: [
{ '@type': 'Offer', name: 'Pro Monthly', price: '2.99', priceCurrency: 'USD', availability: 'https://schema.org/InStock' },
@@ -39,7 +39,7 @@ import PlaneArc from '../components/PlaneArc.astro';
Your call on the rest.
- Free is honest. Pro is generous. No dark patterns, no feature-gated anti-virus, no "unlock export" nonsense — just higher ceilings, longer retention, and iCloud sync for the people who share every day.
+ Launch access keeps the sharing flow generous for everyone: unlimited uploads, 2 GB files, and links up to 30 days. Pro adds iCloud sync, priority support, and Lifetime Family Sharing for people who live in the app every day.
@@ -52,13 +52,13 @@ import PlaneArc from '../components/PlaneArc.astro';
@@ -161,7 +161,7 @@ import PlaneArc from '../components/PlaneArc.astro';
What happens when I downgrade to Free?
-
Existing links keep their original expiry (up to 30 days) — we don't retroactively shorten them. After the subscription lapses, new uploads revert to Free limits: 3/day, 100 MB max, 24 h retention ceiling. iCloud sync stops; records already in your private iCloud stay for 30 days, then iCloud prunes them naturally.
+
Existing links keep their original expiry (up to 30 days) — we don't retroactively shorten them. During launch, new Free uploads keep the same upload ceilings as Pro. iCloud sync stops; records already in your private iCloud stay for 30 days, then iCloud prunes them naturally.
Why $49.99 for Lifetime?
@@ -169,7 +169,7 @@ import PlaneArc from '../components/PlaneArc.astro';
Is there a free trial?
-
No trial in v1 — the Free tier is the trial. It's enough (3 uploads/day, 100 MB, 24 h) to let you decide if the Pro limits actually matter to you before paying. TestFlight and internal beta users may receive temporary unlimited access by allowlist while App Store rollout is active.
+
No trial in v1 — launch access is already generous for core sharing: unlimited uploads, 2 GB files, and up to 30-day links. Pro is for iCloud sync, priority support, and Lifetime Family Sharing.