Как делить авторизацию и платежи между Next.js, Expo и Tauri?
Зависит от того, удастся ли сохранить единую модель идентичности и единое хранилище энтайтлментов. Веб использует сессии на HttpOnly-cookie; Expo после deep-link OAuth-колбэка сохраняет bearer-токен в expo-secure-store; Tauri переиспользует веб-сессию в своём webview — всё это валидируется одним экземпляром Better Auth. Платежи по тому же правилу: 8 веб-шлюзов и RevenueCat IAP для мобайла вливаются в один идемпотентный журнал вебхуков, записывающий энтайтлменты уровня воркспейса, которые читает каждая поверхность.
Два шва, где кроссплатформенный SaaS рвётся
Спросите любую команду, приварившую мобильное приложение к веб-SaaS, где болит по-настоящему — ответ никогда не будет «UI». Это идентичность и деньги: OAuth-флоу, который в браузере возвращается чисто, должен вернуться в приложение через deep link; а клиент, заплативший на сайте, ждёт свой энтайтлмент в телефоне немедленно.
Команды обычно понимают это, выпустив два полурешения: логику cookie, которая тихо ломается внутри WKWebView, и энтайтлмент RevenueCat, воюющий со Stripe-подпиской в веб-БД. Починка архитектурная — и она одна и та же дважды: одна модель идентичности, одно хранилище энтайтлментов.
Один экземпляр Better Auth, три транспорта сессий
supastack держит единую конфигурацию Better Auth в @supastack/auth с точками входа по поверхностям. Оба веб-приложения (Next.js 16 и TanStack Start) используют сессии на HttpOnly-cookie SameSite=Lax — CSRF-безопасные по умолчанию и дружественные к Server Components. Expo-приложение в конце deep-link-колбэка обменивает завершение OAuth или magic link на bearer-токен, сохраняет его в expo-secure-store (iOS Keychain / Android Keystore) и отправляет в заголовке Authorization, валидируемом тем же auth-middleware.
Десктопная оболочка Tauri — единый webview, смотрящий на веб-приложение, поэтому наследует веб-сессию напрямую — никакого третьего auth-флоу для поддержки. Гид по мобильной авторизации и гид по deep links документируют контракт колбэка; наш гид по монорепо веб + мобайл показывает общий клиентский код.
// packages/auth/src/client.ts — one client, two storages
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
export function createCrossPlatformAuthClient(baseURL: string, storage?: any) {
return createAuthClient({
baseURL,
plugins: storage ? [expoClient({ scheme: "myapp", storage })] : [],
});
}
// Web: cookie session. Mobile: bearer token in SecureStore.
// Desktop: the web app's own session inside the Tauri webview.Восемь платёжных шлюзов, один контракт
Каждый веб-провайдер платежей — Stripe, Lemon Squeezy, Polar, Creem, Dodo, Paddle, Whop и PayPal — втыкается в один контракт @supastack/billing, и каждый подписанный вебхук сначала падает в идемпотентный журнал webhook_event, прежде чем ему разрешено трогать энтайтлменты. Ретраи провайдеров и внеочередные доставки становятся на границе БД безобидными холостыми операциями, а не тикетами в поддержку.
Мобильные встроенные покупки вливаются в тот же журнал через RevenueCat: чеки сторов верифицируются на сервере, а энтайтлменты пишутся на воркспейс, а не на устройство — заплативший на iOS сразу оплачен и на вебе, и на десктопе. Гид по RevenueCat описывает подключение App Store / Play; идемпотентные вебхуки биллинга объясняют, почему журнал — несущая деталь.
Модель энтайтлментов, делающая синхронизацию скучной
Почему всё остаётся скучным: энтайтлменты живут на воркспейсе и разрешаются на сервере. Ни один клиент не хранит состояние «оплачено» — веб спрашивает сессию, мобайл — API, десктоп — веб-приложение — и все трое получают один ответ, потому что отвечать может только одна запись.
Восстановление покупок на новом телефоне, продление Stripe на вебе, добавление места Agency в дашборде — каждое есть запись в одни и те же строки энтайтлментов, которые каждая поверхность читает при следующем запросе. Синхронизационный движок строить не нужно, потому что синхронизировать нечего.
| Поверхность | Транспорт сессии | Путь платежа | Энтайтлменты читаются из |
|---|---|---|---|
| Next.js 16 web | HttpOnly-cookie | Любой из 8 шлюзов → журнал вебхуков | Общие строки энтайтлментов Postgres |
| TanStack Start web | HttpOnly-cookie | Те же адаптеры, тот же журнал | Общие строки энтайтлментов Postgres |
| Expo iOS / Android | Bearer-токен в expo-secure-store | RevenueCat IAP → серверная проверка чека | Те же строки (через типизированный API) |
| Tauri 2 desktop | Веб-сессия внутри webview | Веб-чекаут внутри оболочки | Те же строки (через веб-приложение) |
| MV3-расширение | Воркспейс-ключ API | — (читает данные воркспейса) | Те же строки (через /api/v1) |
Что вам не придётся строить
В пересчёте на вечера работы это и есть разница между фундаментом и списком фич: возврат авторизации по deep link, персистентность токена в SecureStore, восемь replay-безопасных интеграций вебхуков, верификация чеков App Store и модель энтайтлментов, никогда не противоречащая себе между устройствами. Всё поставлено, протестировано (693+ тестов без учётных данных) и задокументировано.
Full Bundle за $299 содержит все поверхности в одном репо — количество мест и агентские тарифы на странице цен.
FAQ: общая авторизация и платежи между платформами
Вопросы команд перед выбором единого стека:
- Мобильные пользователи логинятся отдельно от веба?, Нет. OAuth-флоу или magic link возвращается в приложение по deep link и выпускает ту же сессию; выпускает её тот же экземпляр Better Auth, так что веб и мобайл согласны в ответе на «кто вы».
- Что если человек платит на iOS но пользуется веб-приложением?, RevenueCat доставляет покупку на сервере; энтайтлмент записывается в те же строки воркспейса, которые читает веб, и веб-приложение показывает «оплачено» со следующего запроса.
- Можно ли начать только с веба и добавить приложения позже?, Да — авторизация и биллинг живут в общих пакетах, поэтому мобильное и десктопное приложения наследуют их, как только вы включаете эти поверхности.
- Нужен ли Tauri-приложению собственный чекаут?, Нет — десктопная оболочка переиспользует сессию и чекаут веб-приложения внутри своего webview; второй платёжной поверхности для защиты нет.