Skip to main content

Routing to Private Content

You can deep-link to anything in your app — private posts, gated dashboards, user-specific records, paywalled articles, multi-tenant resources. ULink resolves the link to a destination; your app decides who's allowed to see it.

TL;DR

The pattern in one snippet — receive the link, check auth, render or deflect:

ULink.shared.dynamicLinkStream.sink { data in
guard let slug = data.slug else { return }
if AuthService.shared.isSignedIn, PermissionService.shared.canView(slug) {
navigate(to: slug)
} else {
presentSignIn(thenResumeTo: slug)
}
}.store(in: &cancellables)

That's the pattern. The rest of this page explains why it works, the step-by-step, and the rough edges.

ULink is the routing layer. Your app is the authorization layer. The two are deliberately separate.

When a user taps a deep link, ULink resolves it to a destination inside your app — typically an in-app route like /post/:id or a deep-link path like acme://list/abc. ULink doesn't know whether the user is signed in, who they are, or what they're allowed to see. It hands the destination to your app, and your app applies whatever auth and permission rules you've already built.

This is a feature, not a workaround. We don't ship a "ULink-flavored auth" because every team brings their own — Firebase Auth, Supabase, Clerk, Auth0, Cognito, a custom JWT service. An opinionated integration would force you to give up choices that are usually deeply embedded in your app. By staying auth-agnostic, ULink composes cleanly with whatever you already have.

The pattern, step by step

When you create the link, point it at an in-app destination — a route, a slug, or a path that your app already knows how to render. Don't put user IDs, session tokens, or sensitive data in the URL itself.

let params = ULinkParameters.unified(
domain: "links.acme.com",
iosUrl: "acme://post/abc123",
androidUrl: "acme://post/abc123",
fallbackUrl: "https://acme.com/p/abc123"
)
let response = try await ULink.shared.createLink(parameters: params)

Your platform-specific deep-link handler receives the destination from ULink. See the platform guides (iOS, Android, Flutter) for the wiring.

3. Check auth and authorize

Before rendering, run your normal auth and permission checks:

ULink.shared.dynamicLinkStream.sink { data in
guard let slug = data.slug else { return }
switch (AuthService.shared.isSignedIn, PermissionService.shared.canView(slug)) {
case (false, _): deflectToSignIn(targetSlug: slug)
case (true, false): showNotAuthorized()
case (true, true): navigate(to: slug)
}
}.store(in: &cancellables)

The check can be as simple as "is the user signed in" or as rich as a full RBAC/ABAC evaluation. ULink doesn't care.

4. Render or deflect

If access is granted, render the destination. If not, deflect to sign-in, paywall, request-access, or whatever your product does for unauthorized users.

Handling the "user isn't signed in" case

This is the most common rough edge. The user opens a link, your app launches, and the user isn't authenticated. Two patterns work well.

Defer the destination in app state. Store the pending slug somewhere (a singleton, persistent storage, or your state management layer) and replay it after sign-in completes.

final class DeepLinkRouter {
static let shared = DeepLinkRouter()
private var pending: String?

func handle(_ slug: String) {
if AuthService.shared.isSignedIn {
navigate(to: slug)
} else {
pending = slug
AuthService.shared.presentSignIn { [weak self] in
if let s = self?.pending { self?.navigate(to: s); self?.pending = nil }
}
}
}
}

Use deferred deep linking for the install case. When the link is tapped by someone who doesn't have your app installed, ULink can preserve the destination across the install → first-launch boundary. See the platform guides for the deferred-link APIs.

What NOT to put in the URL

The URL is visible to anyone who has the link, plus anyone who scrapes referer headers, social previews, or share-sheet captures. Treat it as public even when the destination is private.

Don'tUse instead
User IDs, account numbersOpaque slugs (abc123) generated server-side
Session tokens, API keysStandard auth headers / cookies in your app
PII (email, phone, SSN)An in-app fetch keyed on the slug, after auth
Payment / order detailsA record ID; fetch the rest after auth

If you need to attach context to a link (e.g. attribution, A/B variant, campaign), use the parameters field — but again, treat it as visible.

Vertical examples

The pattern is identical across verticals; only the auth check differs.

  • B2B SaaS — a shared dashboard link. Your app checks that the recipient is on the workspace and has read permission for the dashboard.
  • Social network — a profile or post link. Your app checks visibility (public / followers-only / private) against the viewer's relationship.
  • Fintech — a transaction or invoice link. Your app re-authenticates with biometrics, then checks that the recipient owns the record. See Fintech use cases for the security framing.
  • Healthcare — a patient record or appointment link. Your app checks HIPAA-compliant access controls. See Health use cases.
  • E-commerce — a wishlist or cart-share link. Your app checks whether the resource was set to "shareable" by the owner, and applies any rate limits.
  • Education — a course module or assignment link. Your app checks enrollment, cohort, and unlock state.

In every case ULink does the same thing: resolve the link, hand off the destination. The differences live entirely on your side, where they should.