create-varity-app 2.0.0-beta.1 → 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/create.js CHANGED
@@ -7,7 +7,7 @@ import ora from "ora";
7
7
  import { getInstallCommand } from "./utils.js";
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
- const VARITY_PACKAGE_VERSION = "^2.0.0-alpha.1";
10
+ const VARITY_PACKAGE_VERSION = "^2.0.0-beta.2";
11
11
  const WORKSPACE_DEPS = {
12
12
  "@varity-labs/sdk": VARITY_PACKAGE_VERSION,
13
13
  "@varity-labs/types": VARITY_PACKAGE_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-varity-app",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.3",
4
4
  "description": "Create production-ready apps with auth, database, and payments built in",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -6,9 +6,9 @@
6
6
  # For production: `varitykit app deploy` injects all credentials automatically.
7
7
  # You never need to manually set these values.
8
8
 
9
- # Auth (optional dev credentials used automatically when blank)
10
- NEXT_PUBLIC_PRIVY_APP_ID=
11
- NEXT_PUBLIC_THIRDWEB_CLIENT_ID=
9
+ # Auth configuration (managed by Varity)
10
+ # Leave blank for dev mode, auto-configured on deploy
11
+ NEXT_PUBLIC_VARITY_AUTH_ID=
12
12
 
13
13
  # Database (optional — dev database used automatically when blank)
14
14
  NEXT_PUBLIC_VARITY_APP_TOKEN=
File without changes
@@ -1,6 +1,6 @@
1
- # Known Issues SaaS Starter Template
1
+ # Known Issues -- SaaS Starter Template
2
2
 
3
- > **Last Updated:** February 14, 2026
3
+ > **Last Updated:** March 4, 2026
4
4
  > **Template:** `saas-starter` (TaskFlow)
5
5
  > **Status:** All features functional, builds with 0 errors (8 routes + _not-found)
6
6
 
@@ -11,17 +11,17 @@
11
11
  | Feature | Status | Notes |
12
12
  |---------|--------|-------|
13
13
  | Landing Page | Working | 6 sections, scroll animations, social proof, testimonials, dashboard mockup |
14
- | Login | Working | Privy email/Google auth, zero-config with dev credentials, auto-redirect |
14
+ | Login | Working | Email/Google auth, zero-config with dev credentials, auto-redirect |
15
15
  | Dashboard | Working | KPI cards, getting started checklist, recent activity feed |
16
16
  | Projects CRUD | Working | Master-detail, nested tasks, CSV export, validation, optimistic updates |
17
17
  | Tasks CRUD | Working | Status cycling (click to advance), filtering, CSV export, cross-references projects |
18
18
  | Team CRUD | Working | Invite members, role management, email validation, role badges |
19
- | Settings | Working | 4 tabs (General, Security, Billing, Account), backend persistence via DB Proxy, skeleton loading |
19
+ | Settings | Working | 4 tabs (General, Security, Billing, Account), backend persistence, skeleton loading |
20
20
  | Command Palette | Working | Cmd+K / Ctrl+K, searches pages and actions |
21
21
  | Toast Notifications | Working | Success/error/info with progress bar, exit animation, max 3 stack |
22
22
  | Protected Routes | Working | Automatic redirect for unauthenticated users |
23
23
  | Color Themes | Working | 4 built-in presets (Blue, Purple, Green, Orange) via CSS variables |
24
- | Static Export | Working | `output: 'export'` for IPFS deployment |
24
+ | Static Export | Working | `output: 'export'` for static hosting deployment |
25
25
  | Mobile Nav | Working | Hamburger menu with responsive sidebar |
26
26
  | CSV Export | Working | One-click export for tasks and projects |
27
27
  | SEO | Working | OpenGraph, Twitter cards, robots.txt, sitemap template |
@@ -29,25 +29,25 @@
29
29
  ## Known Issues
30
30
 
31
31
  ### 1. Navigation Flash (UI-Kit Limitation)
32
- Brief "Initializing Dashboard" screen when navigating between dashboard pages. Caused by `PrivyReadyGate` in UI-Kit re-checking auth state on each route change. Resolves in <1 second. This is a UI-Kit issue, not template-side.
32
+ Brief "Initializing Dashboard" screen when navigating between dashboard pages. Caused by the auth guard in UI-Kit re-checking auth state on each route change. Resolves in <1 second. This is a UI-Kit issue, not template-side.
33
33
 
34
34
  ### 2. DashboardLayout Mobile Support
35
- The `DashboardLayout` from `@varity-labs/ui-kit` does not include mobile navigation. The template provides its own responsive sidebar in `src/app/dashboard/layout.tsx` as a workaround. UI-Kit mobile improvements are planned post-MVP.
35
+ The `DashboardLayout` from `@varity-labs/ui-kit` does not include mobile navigation. The template provides its own responsive sidebar in `src/app/dashboard/layout.tsx` as a workaround. UI-Kit mobile improvements are planned.
36
36
 
37
37
  ### 3. Billing Section is Mock
38
38
  The Settings > Billing tab shows a mock UI (plan name, usage bars, payment method). Developers should wire their own billing provider (Stripe, etc.).
39
39
 
40
40
  ### 4. Sessions Are Mock
41
- The Settings > Security tab shows active sessions with a "Revoke" button. Session data is client-side mock real session management is handled by Privy (the auth provider).
41
+ The Settings > Security tab shows active sessions with a "Revoke" button. Session data is client-side mock -- real session management is handled by the auth provider.
42
42
 
43
43
  ### 5. Password & Profile Managed by Auth Provider
44
- "Change password" and profile photo are managed by Privy (the auth provider). The Settings page shows informational dialogs explaining this.
44
+ "Change password" and profile photo are managed by the authentication provider. The Settings page shows informational dialogs explaining this.
45
45
 
46
46
  ### 6. No Server-Side Rendering
47
47
  All pages are statically exported (`output: 'export'`). No server-side rendering, API routes, or middleware. Data fetching happens client-side via the SDK.
48
48
 
49
49
  ### 7. Team Email Invites
50
- No SMTP integration team members are added to the database only. No invitation email is sent. Developers should integrate their own email service.
50
+ No SMTP integration -- team members are added to the database only. No invitation email is sent. Developers should integrate their own email service.
51
51
 
52
52
  ## Environment
53
53
 
@@ -1,8 +1,8 @@
1
- # TaskFlow SaaS Starter Template
1
+ # TaskFlow -- SaaS Starter Template
2
2
 
3
3
  [![Built with Varity](https://img.shields.io/badge/built%20with-Varity-7C3AED)](https://varity.so)
4
4
 
5
- A full-featured project management app built with [Varity](https://varity.so). Everything works immediately no configuration, no API keys, no setup.
5
+ A full-featured project management app built with [Varity](https://varity.so). Everything works immediately -- no configuration, no API keys, no setup.
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -23,12 +23,12 @@ No `.env` file needed. No accounts to create. No credentials to configure.
23
23
 
24
24
  Transform this into your own branded SaaS app:
25
25
 
26
- 1. **App name** Edit `APP_NAME` in `src/lib/constants.ts`
27
- 2. **Logo** Replace `public/logo.svg` with your logo
28
- 3. **Colors** Open `src/app/globals.css` and uncomment a color preset (Purple, Green, or Orange) or set your own
29
- 4. **Meta title** Update the `title` and `description` in `src/app/layout.tsx`
30
- 5. **Navigation** Edit `NAVIGATION_ITEMS` in `src/lib/constants.ts` to rename or add sidebar links
31
- 6. **Landing page** Edit the sections in `src/components/landing/` (Hero, Features, Pricing, etc.)
26
+ 1. **App name** -- Edit `APP_NAME` in `src/lib/constants.ts`
27
+ 2. **Logo** -- Replace `public/logo.svg` with your logo
28
+ 3. **Colors** -- Open `src/app/globals.css` and uncomment a color preset (Purple, Green, or Orange) or set your own
29
+ 4. **Meta title** -- Update the `title` and `description` in `src/app/layout.tsx`
30
+ 5. **Navigation** -- Edit `NAVIGATION_ITEMS` in `src/lib/constants.ts` to rename or add sidebar links
31
+ 6. **Landing page** -- Edit the sections in `src/components/landing/` (Hero, Features, Pricing, etc.)
32
32
 
33
33
  ## Built-in Color Themes
34
34
 
@@ -44,71 +44,53 @@ Switch your entire app's color scheme by editing `src/app/globals.css`:
44
44
 
45
45
  ## What's Included
46
46
 
47
- - **Zero-Config Auth** Email and social login works out of the box
48
- - **Zero-Config Database** Data persistence with isolated dev environment
49
- - **Dashboard** KPI cards, data tables, status badges, getting started guide
50
- - **Full CRUD** Create, read, update, delete for projects, tasks, and team members
51
- - **Command Palette** Cmd+K search across all data
52
- - **Protected Routes** Automatic redirect for unauthenticated users
53
- - **Landing Page** Professional marketing page with hero, features, pricing, testimonials
54
- - **Mobile Responsive** Hamburger menu, responsive layouts, touch-friendly
55
- - **TypeScript** Full type safety throughout
56
- - **Tailwind CSS** Utility-first styling with CSS variable theming
47
+ - **Zero-Config Auth** -- Email and social login works out of the box
48
+ - **Zero-Config Database** -- Data persistence with isolated dev environment
49
+ - **Dashboard** -- KPI cards, data tables, status badges, getting started guide
50
+ - **Full CRUD** -- Create, read, update, delete for projects, tasks, and team members
51
+ - **Command Palette** -- Cmd+K search across all data
52
+ - **Protected Routes** -- Automatic redirect for unauthenticated users
53
+ - **Landing Page** -- Professional marketing page with hero, features, pricing, testimonials
54
+ - **Mobile Responsive** -- Hamburger menu, responsive layouts, touch-friendly
55
+ - **TypeScript** -- Full type safety throughout
56
+ - **Tailwind CSS** -- Utility-first styling with CSS variable theming
57
57
 
58
- ## Zero Configuration Required
58
+ ## Zero Configuration Required
59
59
 
60
60
  This template works immediately with **zero setup**:
61
61
 
62
62
  ### Instant Auth
63
- - Email login (Privy)
64
- - Google/Apple social login
65
- - Dev credentials built-in
66
- - No env vars needed
63
+ - Email login (email + Google)
64
+ - Social login support
65
+ - Dev credentials built-in
66
+ - No env vars needed
67
67
 
68
68
  ### Instant Database
69
- - Create, read, update, delete data
70
- - Dev token built-in
71
- - Production-ready proxy
72
- - No credentials needed
69
+ - Create, read, update, delete data
70
+ - Dev token built-in
71
+ - Production-ready
72
+ - No credentials needed
73
73
 
74
74
  ### Instant Deploy
75
75
  ```bash
76
- npm run deploy
76
+ varitykit app deploy
77
77
  ```
78
- - Deploys to IPFS
79
- - Auto-fetches credentials
80
- - No thirdweb account needed
78
+ - Deploys to production
79
+ - Auto-configures credentials
80
+ - No accounts needed
81
81
 
82
82
  ---
83
83
 
84
- ## 🏗️ Architecture
85
-
86
- ### Workspace Dependencies
87
- This template uses `workspace:^` protocol for Varity packages:
88
- ```json
89
- {
90
- "dependencies": {
91
- "@varity-labs/sdk": "workspace:^",
92
- "@varity-labs/ui-kit": "workspace:^",
93
- "@varity-labs/types": "workspace:^"
94
- }
95
- }
96
- ```
97
-
98
- **Why?** Ensures you always use the latest local package versions during development.
99
-
100
- **Publishing:** When published to npm, `workspace:^` converts to `^2.0.0-alpha.1` automatically.
101
-
102
84
  ### Static Export Ready
103
- - `output: 'export'` in next.config.js
104
- - All pages pre-rendered to static HTML
105
- - No server-side dependencies
106
- - ✅ IPFS/CDN deployable
85
+ - `output: 'export'` in next.config.js
86
+ - All pages pre-rendered to static HTML
87
+ - No server-side dependencies
88
+ - CDN deployable
107
89
 
108
90
  ### Type Safety
109
- - TypeScript strict mode enabled
110
- - All errors surface during build
111
- - No `ignoreBuildErrors` flag
91
+ - TypeScript strict mode enabled
92
+ - All errors surface during build
93
+ - No `ignoreBuildErrors` flag
112
94
 
113
95
  ## Project Structure
114
96
 
@@ -205,18 +187,17 @@ export function useInvoices(): UseCollectionReturn<Invoice> {
205
187
  const { data, loading, create, update, remove } = useInvoices();
206
188
  ```
207
189
 
208
- The database collection is created automatically on first use no migrations needed.
190
+ The database collection is created automatically on first use -- no migrations needed.
209
191
 
210
192
  ## Environment Variables
211
193
 
212
194
  **For development:** Leave everything blank. Shared development credentials are used automatically.
213
195
 
214
- **For production:** Run `varitykit app deploy` it injects all credentials into your build automatically. You never need to manually set API keys.
196
+ **For production:** Run `varitykit app deploy` -- it injects all credentials into your build automatically. You never need to manually set API keys.
215
197
 
216
198
  | Variable | Required | Notes |
217
199
  |----------|----------|-------|
218
200
  | `NEXT_PUBLIC_PRIVY_APP_ID` | No | Auth provider (auto-configured) |
219
- | `NEXT_PUBLIC_THIRDWEB_CLIENT_ID` | No | Infrastructure (auto-configured) |
220
201
  | `NEXT_PUBLIC_VARITY_APP_TOKEN` | No | Database token (auto-configured) |
221
202
  | `NEXT_PUBLIC_VARITY_APP_ID` | No | App ID (auto-configured) |
222
203
 
@@ -230,7 +211,7 @@ varitykit app deploy
230
211
  varitykit app deploy --submit-to-store
231
212
  ```
232
213
 
233
- The CLI builds your app, provisions a private database, injects production credentials, and deploys all in one command.
214
+ The CLI builds your app, provisions a private database, injects production credentials, and deploys -- all in one command.
234
215
 
235
216
  **Deploy from your AI editor:** Set up the [Varity MCP server](https://docs.varity.so/mcp) (`npx @varity-labs/mcp`) and ask your AI to "deploy this project".
236
217
 
@@ -5,7 +5,7 @@ const nextConfig = {
5
5
  trailingSlash: true,
6
6
  productionBrowserSourceMaps: false,
7
7
  webpack: (config, { isServer, dev }) => {
8
- // Suppress MetaMask SDK warning for @react-native-async-storage
8
+ // Suppress async-storage warning from auth provider dependencies
9
9
  config.resolve.fallback = {
10
10
  ...config.resolve.fallback,
11
11
  '@react-native-async-storage/async-storage': false,
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@varity-labs/sdk": "workspace:^",
21
21
  "@varity-labs/types": "workspace:^",
22
- "@varity-labs/ui-kit": "workspace:^",
22
+ "@varity-labs/ui-kit": "^2.0.0-beta.3",
23
23
  "lucide-react": "^0.400.0",
24
24
  "next": "^15.0.0",
25
25
  "react": "^18.3.0",
@@ -7,18 +7,20 @@ import { useProjects, useTasks, useTeam } from '@/lib/hooks';
7
7
  import { CommandPalette } from '@varity-labs/ui-kit';
8
8
  import { Menu, X } from 'lucide-react';
9
9
 
10
- // Conditionally import Privy/UI-Kit components
10
+ // Conditionally import UI-Kit components
11
11
  let DashboardLayout: any = null;
12
- let PrivyProtectedRoute: any = null;
13
- let PrivyStackComponent: any = null;
14
- let usePrivyHook: any = null;
12
+ let ProtectedRoute: any = null;
13
+ let AuthStackComponent: any = null;
14
+ let ZeroDevProviderComponent: any = null;
15
+ let useAuthHook: any = null;
15
16
 
16
17
  try {
17
18
  const uiKit = require('@varity-labs/ui-kit');
18
19
  DashboardLayout = uiKit.DashboardLayout;
19
- PrivyProtectedRoute = uiKit.PrivyProtectedRoute;
20
- PrivyStackComponent = uiKit.PrivyStack;
21
- usePrivyHook = uiKit.usePrivy;
20
+ ProtectedRoute = uiKit.PrivyProtectedRoute;
21
+ AuthStackComponent = uiKit.PrivyStack;
22
+ ZeroDevProviderComponent = uiKit.ZeroDevProvider;
23
+ useAuthHook = uiKit.useAuth;
22
24
  } catch {}
23
25
 
24
26
  function RedirectToLogin() {
@@ -111,8 +113,8 @@ function MobileNav({
111
113
 
112
114
  function DashboardShell({ children }: { children: React.ReactNode }) {
113
115
  // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
114
- const privy = usePrivyHook ? usePrivyHook() : { user: null, logout: async () => {} };
115
- const { user, logout } = privy;
116
+ const auth = useAuthHook ? useAuthHook() : { user: null, logout: async () => {} };
117
+ const { user, logout } = auth;
116
118
  const pathname = usePathname();
117
119
  const router = useRouter();
118
120
  const isMobile = useIsMobile();
@@ -152,7 +154,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
152
154
  const userName = user?.email?.address?.split('@')[0] || 'User';
153
155
  const userEmail = user?.email?.address || '';
154
156
 
155
- const handleLogout = async () => {
157
+ const handleSignOut = async () => {
156
158
  await logout();
157
159
  router.push('/');
158
160
  };
@@ -194,7 +196,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
194
196
  </nav>
195
197
  <div className="mt-8 px-3 border-t border-gray-200 pt-4">
196
198
  <button
197
- onClick={handleLogout}
199
+ onClick={handleSignOut}
198
200
  className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-red-600 hover:bg-red-50 transition-colors"
199
201
  >
200
202
  Sign Out
@@ -210,7 +212,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
210
212
  onClose={() => setMobileMenuOpen(false)}
211
213
  navItems={navWithActive}
212
214
  userEmail={userEmail}
213
- onLogout={handleLogout}
215
+ onLogout={handleSignOut}
214
216
  onNavigate={(path) => router.push(path)}
215
217
  />
216
218
  )}
@@ -241,7 +243,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
241
243
  onClose={() => setMobileMenuOpen(false)}
242
244
  navItems={navWithActive}
243
245
  userEmail={userEmail}
244
- onLogout={handleLogout}
246
+ onLogout={handleSignOut}
245
247
  onNavigate={(path) => router.push(path)}
246
248
  />
247
249
  )}
@@ -257,7 +259,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
257
259
  name: userName,
258
260
  address: userEmail,
259
261
  }}
260
- onLogout={handleLogout}
262
+ onLogout={handleSignOut}
261
263
  onNavigateToProfile={() => router.push('/dashboard/settings')}
262
264
  onNavigateToSettings={() => router.push('/dashboard/settings')}
263
265
  onSearchClick={() => setCommandPaletteOpen(true)}
@@ -277,22 +279,23 @@ export default function DashboardRootLayout({
277
279
  }: {
278
280
  children: React.ReactNode;
279
281
  }) {
280
- // Always wrap in PrivyStack + PrivyProtectedRoute - uses dev credentials automatically when env vars are empty
281
- if (!PrivyProtectedRoute || !PrivyStackComponent) {
282
+ // Auth provider (managed by Varity) with gas sponsorship
283
+ if (!ProtectedRoute || !AuthStackComponent || !ZeroDevProviderComponent) {
282
284
  // Fallback if ui-kit package isn't installed
283
285
  return <DashboardShell>{children}</DashboardShell>;
284
286
  }
285
287
 
286
288
  return (
287
- <PrivyStackComponent
288
- appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
289
- thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
289
+ <AuthStackComponent
290
+ appId={process.env.NEXT_PUBLIC_VARITY_AUTH_ID}
290
291
  loginMethods={['email', 'google']}
291
292
  appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
292
293
  >
293
- <PrivyProtectedRoute fallback={<RedirectToLogin />}>
294
- <DashboardShell>{children}</DashboardShell>
295
- </PrivyProtectedRoute>
296
- </PrivyStackComponent>
294
+ <ZeroDevProviderComponent>
295
+ <ProtectedRoute fallback={<RedirectToLogin />}>
296
+ <DashboardShell>{children}</DashboardShell>
297
+ </ProtectedRoute>
298
+ </ZeroDevProviderComponent>
299
+ </AuthStackComponent>
297
300
  );
298
301
  }
@@ -37,15 +37,15 @@ import {
37
37
  Key,
38
38
  } from 'lucide-react';
39
39
 
40
- const privyAppId = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
40
+ const authAppId = process.env.NEXT_PUBLIC_VARITY_AUTH_ID;
41
41
 
42
- let PrivyUserProfile: any = null;
43
- let usePrivyHook: any = null;
42
+ let UserProfileComponent: any = null;
43
+ let useAuthHook: any = null;
44
44
 
45
45
  try {
46
46
  const uiKit = require('@varity-labs/ui-kit');
47
- PrivyUserProfile = uiKit.PrivyUserProfile;
48
- usePrivyHook = uiKit.usePrivy;
47
+ UserProfileComponent = uiKit.PrivyUserProfile;
48
+ useAuthHook = uiKit.useAuth;
49
49
  } catch {}
50
50
 
51
51
  const TABS = [
@@ -79,7 +79,7 @@ export default function SettingsPage() {
79
79
  const { settings, loading, update: updateSettings } = useUserSettings();
80
80
  const toast = useToast();
81
81
  // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() + env var, stable across renders
82
- const privy = privyAppId && usePrivyHook ? usePrivyHook() : { logout: async () => {} };
82
+ const auth = authAppId && useAuthHook ? useAuthHook() : { logout: async () => {} };
83
83
 
84
84
  const [activeTab, setActiveTab] = useState<TabId>('general');
85
85
 
@@ -556,14 +556,14 @@ export default function SettingsPage() {
556
556
  )}
557
557
  </div>
558
558
 
559
- {/* Privy Account Settings */}
560
- {privyAppId && PrivyUserProfile && (
559
+ {/* Advanced Account Settings */}
560
+ {authAppId && UserProfileComponent && (
561
561
  <div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
562
562
  <h2 className="mb-4 text-lg font-semibold text-gray-900 flex items-center gap-2">
563
563
  <Key className="h-5 w-5" />
564
- Account Settings (Privy)
564
+ Advanced Account Settings
565
565
  </h2>
566
- <PrivyUserProfile showLogoutButton={false} />
566
+ <UserProfileComponent showLogoutButton={false} />
567
567
  </div>
568
568
  )}
569
569
 
@@ -577,7 +577,7 @@ export default function SettingsPage() {
577
577
  These actions are permanent and cannot be undone.
578
578
  </p>
579
579
  <div className="flex flex-wrap gap-3">
580
- <Button variant="danger" onClick={() => privy.logout()}>
580
+ <Button variant="danger" onClick={() => auth.logout()}>
581
581
  <LogOut className="h-4 w-4" />
582
582
  Sign Out
583
583
  </Button>
@@ -5,7 +5,7 @@ import './globals.css';
5
5
  export const metadata: Metadata = {
6
6
  title: 'TaskFlow - Project Management',
7
7
  description: 'Manage projects, track tasks, and collaborate with your team.',
8
- metadataBase: new URL('https://example.com'),
8
+ metadataBase: new URL('https://varity.so'),
9
9
  openGraph: {
10
10
  title: 'TaskFlow - Project Management',
11
11
  description: 'Manage projects, track tasks, and collaborate with your team.',
@@ -6,29 +6,31 @@ import Link from 'next/link';
6
6
  import { CheckCircle } from 'lucide-react';
7
7
  import { APP_NAME } from '@/lib/constants';
8
8
 
9
- let PrivyStackComponent: any = null;
10
- let usePrivyHook: (() => { authenticated: boolean; ready: boolean; login: () => void }) | null = null;
9
+ let AuthStackComponent: any = null;
10
+ let ZeroDevProviderComponent: any = null;
11
+ let useAuthHook: (() => { authenticated: boolean; ready: boolean; login: () => void }) | null = null;
11
12
 
12
13
  try {
13
14
  const uiKit = require('@varity-labs/ui-kit');
14
- PrivyStackComponent = uiKit.PrivyStack;
15
- usePrivyHook = uiKit.usePrivy;
15
+ AuthStackComponent = uiKit.PrivyStack;
16
+ ZeroDevProviderComponent = uiKit.ZeroDevProvider;
17
+ useAuthHook = uiKit.useAuth;
16
18
  } catch {}
17
19
 
18
20
  function LoginContent() {
19
21
  const router = useRouter();
20
22
  // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
21
- const privy = usePrivyHook ? usePrivyHook() : null;
23
+ const auth = useAuthHook ? useAuthHook() : null;
22
24
 
23
25
  useEffect(() => {
24
- if (privy?.authenticated) {
26
+ if (auth?.authenticated) {
25
27
  router.push('/dashboard');
26
28
  }
27
- }, [privy?.authenticated, router]);
29
+ }, [auth?.authenticated, router]);
28
30
 
29
- const handleLogin = () => {
30
- if (privy?.login) {
31
- privy.login();
31
+ const handleSignIn = () => {
32
+ if (auth?.login) {
33
+ auth.login();
32
34
  }
33
35
  };
34
36
 
@@ -49,15 +51,15 @@ function LoginContent() {
49
51
  </div>
50
52
 
51
53
  <div className="rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
52
- {privy ? (
54
+ {auth ? (
53
55
  <button
54
- onClick={handleLogin}
55
- disabled={!privy.ready || privy.authenticated}
56
+ onClick={handleSignIn}
57
+ disabled={!auth.ready || auth.authenticated}
56
58
  className="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors shadow-sm"
57
59
  >
58
- {!privy.ready
60
+ {!auth.ready
59
61
  ? 'Loading...'
60
- : privy.authenticated
62
+ : auth.authenticated
61
63
  ? 'Already Signed In'
62
64
  : 'Sign In with Email or Social'}
63
65
  </button>
@@ -79,17 +81,18 @@ function LoginContent() {
79
81
  }
80
82
 
81
83
  export default function LoginPage() {
82
- // Always wrap in PrivyStack - it uses dev credentials automatically when no appId is provided
83
- if (PrivyStackComponent) {
84
+ // Auth provider (managed by Varity) with gas sponsorship
85
+ if (AuthStackComponent && ZeroDevProviderComponent) {
84
86
  return (
85
- <PrivyStackComponent
86
- appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
87
- thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}
87
+ <AuthStackComponent
88
+ appId={process.env.NEXT_PUBLIC_VARITY_AUTH_ID}
88
89
  loginMethods={['email', 'google']}
89
90
  appearance={{ theme: 'light', accentColor: '#2563EB', logo: '/logo.svg' }}
90
91
  >
91
- <LoginContent />
92
- </PrivyStackComponent>
92
+ <ZeroDevProviderComponent>
93
+ <LoginContent />
94
+ </ZeroDevProviderComponent>
95
+ </AuthStackComponent>
93
96
  );
94
97
  }
95
98
 
@@ -5,7 +5,7 @@ import { ToastProvider } from '@varity-labs/ui-kit';
5
5
 
6
6
  export function Providers({ children }: { children: ReactNode }) {
7
7
  // Only ToastProvider at the global level.
8
- // PrivyStack is added in dashboard/layout.tsx and login/page.tsx —
8
+ // Auth provider is added in dashboard/layout.tsx and login/page.tsx —
9
9
  // the landing page loads instantly without any auth dependency.
10
10
  return <ToastProvider>{children}</ToastProvider>;
11
11
  }
@@ -4,18 +4,18 @@ import { useState, useEffect, useCallback } from 'react';
4
4
  import { projects, tasks, teamMembers, userSettings } from './database';
5
5
  import type { Project, Task, TeamMember, UserSettings } from '../types';
6
6
 
7
- let usePrivyHook: any = null;
7
+ let useAuthHook: any = null;
8
8
  try {
9
9
  const uiKit = require('@varity-labs/ui-kit');
10
- usePrivyHook = uiKit.usePrivy;
10
+ useAuthHook = uiKit.useAuth;
11
11
  } catch {}
12
12
 
13
13
  export function useCurrentUser() {
14
14
  // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on require() success, stable across renders
15
- const privy = usePrivyHook ? usePrivyHook() : { user: null, authenticated: false, logout: async () => {} };
15
+ const auth = useAuthHook ? useAuthHook() : { user: null, authenticated: false, logout: async () => {} };
16
16
 
17
- // Extract email from any Privy auth method (email, Google, GitHub, etc.)
18
- const user = privy.user;
17
+ // Extract email from any auth method (email, Google, GitHub, etc.)
18
+ const user = auth.user;
19
19
  const email =
20
20
  user?.email?.address ||
21
21
  user?.google?.email ||
@@ -30,8 +30,8 @@ export function useCurrentUser() {
30
30
  id: user?.id || 'dev-user-id',
31
31
  email,
32
32
  name,
33
- authenticated: privy.authenticated,
34
- logout: privy.logout,
33
+ authenticated: auth.authenticated,
34
+ logout: auth.logout,
35
35
  };
36
36
  }
37
37
 
@@ -2,7 +2,7 @@
2
2
  "name": "my-saas-app",
3
3
  "version": "0.1.0",
4
4
  "framework": "nextjs",
5
- "hosting": "ipfs",
5
+ "hosting": "static",
6
6
  "build": {
7
7
  "command": "npm run build",
8
8
  "output": "out"
@@ -1,589 +0,0 @@
1
- /**
2
- * Dashboard Service
3
- *
4
- * Centralized service for dashboard-related API calls.
5
- * Provides type-safe interfaces and error handling.
6
- */
7
-
8
- import type { Project, Task, TeamMember } from '@/types';
9
-
10
- // ============================================================================
11
- // Type Definitions
12
- // ============================================================================
13
-
14
- export interface KPIMetric {
15
- title: string;
16
- value: string | number;
17
- change?: {
18
- value: number;
19
- period: string;
20
- };
21
- trend?: 'up' | 'down' | 'neutral';
22
- sparklineData?: number[];
23
- }
24
-
25
- export interface KPIResponse {
26
- kpis: KPIMetric[];
27
- has_data: boolean;
28
- last_updated: string;
29
- }
30
-
31
- export interface Activity {
32
- id: string;
33
- type: 'project_created' | 'task_completed' | 'member_added' | 'comment_added';
34
- title: string;
35
- description: string;
36
- timestamp: string;
37
- user?: {
38
- name: string;
39
- avatar?: string;
40
- };
41
- metadata?: Record<string, any>;
42
- }
43
-
44
- export interface ActivityResponse {
45
- activities: Activity[];
46
- total_count: number;
47
- has_more: boolean;
48
- }
49
-
50
- export interface ProjectResponse {
51
- projects: Project[];
52
- total_count: number;
53
- active_count: number;
54
- }
55
-
56
- export interface TaskResponse {
57
- tasks: Task[];
58
- total_count: number;
59
- completed_count: number;
60
- by_status: {
61
- todo: number;
62
- in_progress: number;
63
- done: number;
64
- };
65
- }
66
-
67
- export interface TeamMemberResponse {
68
- members: TeamMember[];
69
- total_count: number;
70
- roles: {
71
- owner: number;
72
- admin: number;
73
- member: number;
74
- viewer: number;
75
- };
76
- }
77
-
78
- export interface DashboardOverviewResponse {
79
- kpis: KPIResponse;
80
- recent_activity: Activity[];
81
- projects_summary: {
82
- active: number;
83
- total: number;
84
- recent: Project[];
85
- };
86
- tasks_summary: {
87
- open: number;
88
- completed: number;
89
- completion_rate: number;
90
- };
91
- team_summary: {
92
- total: number;
93
- active: number;
94
- };
95
- }
96
-
97
- // ============================================================================
98
- // Error Handling
99
- // ============================================================================
100
-
101
- export class DashboardServiceError extends Error {
102
- constructor(
103
- message: string,
104
- public statusCode?: number,
105
- public originalError?: Error
106
- ) {
107
- super(message);
108
- this.name = 'DashboardServiceError';
109
- }
110
- }
111
-
112
- async function handleResponse<T>(response: Response): Promise<T> {
113
- if (!response.ok) {
114
- const errorText = await response.text().catch(() => 'Unknown error');
115
- throw new DashboardServiceError(
116
- `API request failed: ${response.statusText}`,
117
- response.status,
118
- new Error(errorText)
119
- );
120
- }
121
-
122
- try {
123
- return await response.json();
124
- } catch (error) {
125
- throw new DashboardServiceError(
126
- 'Failed to parse API response',
127
- response.status,
128
- error instanceof Error ? error : undefined
129
- );
130
- }
131
- }
132
-
133
- // ============================================================================
134
- // API Client
135
- // ============================================================================
136
-
137
- const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api';
138
-
139
- /**
140
- * Fetch dashboard KPIs
141
- */
142
- export async function getKPIs(userId: string): Promise<KPIResponse> {
143
- try {
144
- const response = await fetch(
145
- `${API_BASE_URL}/dashboard/kpis?userId=${encodeURIComponent(userId)}`,
146
- {
147
- method: 'GET',
148
- headers: {
149
- 'Content-Type': 'application/json',
150
- },
151
- cache: 'no-store', // Always fetch fresh data
152
- }
153
- );
154
-
155
- return await handleResponse<KPIResponse>(response);
156
- } catch (error) {
157
- if (error instanceof DashboardServiceError) {
158
- throw error;
159
- }
160
- throw new DashboardServiceError(
161
- 'Failed to fetch KPIs',
162
- undefined,
163
- error instanceof Error ? error : undefined
164
- );
165
- }
166
- }
167
-
168
- /**
169
- * Fetch recent activity
170
- */
171
- export async function getRecentActivity(
172
- userId: string,
173
- limit: number = 10
174
- ): Promise<Activity[]> {
175
- try {
176
- const response = await fetch(
177
- `${API_BASE_URL}/dashboard/activity?userId=${encodeURIComponent(userId)}&limit=${limit}`,
178
- {
179
- method: 'GET',
180
- headers: {
181
- 'Content-Type': 'application/json',
182
- },
183
- cache: 'no-store',
184
- }
185
- );
186
-
187
- const data = await handleResponse<ActivityResponse>(response);
188
- return data.activities;
189
- } catch (error) {
190
- if (error instanceof DashboardServiceError) {
191
- throw error;
192
- }
193
- throw new DashboardServiceError(
194
- 'Failed to fetch recent activity',
195
- undefined,
196
- error instanceof Error ? error : undefined
197
- );
198
- }
199
- }
200
-
201
- /**
202
- * Fetch all projects for a user
203
- */
204
- export async function getProjects(userId: string): Promise<Project[]> {
205
- try {
206
- const response = await fetch(
207
- `${API_BASE_URL}/projects?userId=${encodeURIComponent(userId)}`,
208
- {
209
- method: 'GET',
210
- headers: {
211
- 'Content-Type': 'application/json',
212
- },
213
- cache: 'no-store',
214
- }
215
- );
216
-
217
- const data = await handleResponse<ProjectResponse>(response);
218
- return data.projects;
219
- } catch (error) {
220
- if (error instanceof DashboardServiceError) {
221
- throw error;
222
- }
223
- throw new DashboardServiceError(
224
- 'Failed to fetch projects',
225
- undefined,
226
- error instanceof Error ? error : undefined
227
- );
228
- }
229
- }
230
-
231
- /**
232
- * Fetch tasks, optionally filtered by project
233
- */
234
- export async function getTasks(
235
- userId: string,
236
- projectId?: string
237
- ): Promise<Task[]> {
238
- try {
239
- const url = projectId
240
- ? `${API_BASE_URL}/tasks?userId=${encodeURIComponent(userId)}&projectId=${encodeURIComponent(projectId)}`
241
- : `${API_BASE_URL}/tasks?userId=${encodeURIComponent(userId)}`;
242
-
243
- const response = await fetch(url, {
244
- method: 'GET',
245
- headers: {
246
- 'Content-Type': 'application/json',
247
- },
248
- cache: 'no-store',
249
- });
250
-
251
- const data = await handleResponse<TaskResponse>(response);
252
- return data.tasks;
253
- } catch (error) {
254
- if (error instanceof DashboardServiceError) {
255
- throw error;
256
- }
257
- throw new DashboardServiceError(
258
- 'Failed to fetch tasks',
259
- undefined,
260
- error instanceof Error ? error : undefined
261
- );
262
- }
263
- }
264
-
265
- /**
266
- * Fetch team members
267
- */
268
- export async function getTeamMembers(userId: string): Promise<TeamMember[]> {
269
- try {
270
- const response = await fetch(
271
- `${API_BASE_URL}/team?userId=${encodeURIComponent(userId)}`,
272
- {
273
- method: 'GET',
274
- headers: {
275
- 'Content-Type': 'application/json',
276
- },
277
- cache: 'no-store',
278
- }
279
- );
280
-
281
- const data = await handleResponse<TeamMemberResponse>(response);
282
- return data.members;
283
- } catch (error) {
284
- if (error instanceof DashboardServiceError) {
285
- throw error;
286
- }
287
- throw new DashboardServiceError(
288
- 'Failed to fetch team members',
289
- undefined,
290
- error instanceof Error ? error : undefined
291
- );
292
- }
293
- }
294
-
295
- /**
296
- * Fetch complete dashboard overview
297
- */
298
- export async function getDashboardOverview(
299
- userId: string
300
- ): Promise<DashboardOverviewResponse> {
301
- try {
302
- const response = await fetch(
303
- `${API_BASE_URL}/dashboard/overview?userId=${encodeURIComponent(userId)}`,
304
- {
305
- method: 'GET',
306
- headers: {
307
- 'Content-Type': 'application/json',
308
- },
309
- cache: 'no-store',
310
- }
311
- );
312
-
313
- return await handleResponse<DashboardOverviewResponse>(response);
314
- } catch (error) {
315
- if (error instanceof DashboardServiceError) {
316
- throw error;
317
- }
318
- throw new DashboardServiceError(
319
- 'Failed to fetch dashboard overview',
320
- undefined,
321
- error instanceof Error ? error : undefined
322
- );
323
- }
324
- }
325
-
326
- /**
327
- * Create a new project
328
- */
329
- export async function createProject(
330
- userId: string,
331
- data: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>
332
- ): Promise<Project> {
333
- try {
334
- const response = await fetch(`${API_BASE_URL}/projects`, {
335
- method: 'POST',
336
- headers: {
337
- 'Content-Type': 'application/json',
338
- },
339
- body: JSON.stringify({ userId, ...data }),
340
- });
341
-
342
- return await handleResponse<Project>(response);
343
- } catch (error) {
344
- if (error instanceof DashboardServiceError) {
345
- throw error;
346
- }
347
- throw new DashboardServiceError(
348
- 'Failed to create project',
349
- undefined,
350
- error instanceof Error ? error : undefined
351
- );
352
- }
353
- }
354
-
355
- /**
356
- * Update an existing project
357
- */
358
- export async function updateProject(
359
- projectId: string,
360
- data: Partial<Project>
361
- ): Promise<Project> {
362
- try {
363
- const response = await fetch(`${API_BASE_URL}/projects/${projectId}`, {
364
- method: 'PATCH',
365
- headers: {
366
- 'Content-Type': 'application/json',
367
- },
368
- body: JSON.stringify(data),
369
- });
370
-
371
- return await handleResponse<Project>(response);
372
- } catch (error) {
373
- if (error instanceof DashboardServiceError) {
374
- throw error;
375
- }
376
- throw new DashboardServiceError(
377
- 'Failed to update project',
378
- undefined,
379
- error instanceof Error ? error : undefined
380
- );
381
- }
382
- }
383
-
384
- /**
385
- * Delete a project
386
- */
387
- export async function deleteProject(projectId: string): Promise<void> {
388
- try {
389
- const response = await fetch(`${API_BASE_URL}/projects/${projectId}`, {
390
- method: 'DELETE',
391
- headers: {
392
- 'Content-Type': 'application/json',
393
- },
394
- });
395
-
396
- if (!response.ok) {
397
- throw new DashboardServiceError(
398
- `Failed to delete project: ${response.statusText}`,
399
- response.status
400
- );
401
- }
402
- } catch (error) {
403
- if (error instanceof DashboardServiceError) {
404
- throw error;
405
- }
406
- throw new DashboardServiceError(
407
- 'Failed to delete project',
408
- undefined,
409
- error instanceof Error ? error : undefined
410
- );
411
- }
412
- }
413
-
414
- /**
415
- * Create a new task
416
- */
417
- export async function createTask(
418
- userId: string,
419
- data: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>
420
- ): Promise<Task> {
421
- try {
422
- const response = await fetch(`${API_BASE_URL}/tasks`, {
423
- method: 'POST',
424
- headers: {
425
- 'Content-Type': 'application/json',
426
- },
427
- body: JSON.stringify({ userId, ...data }),
428
- });
429
-
430
- return await handleResponse<Task>(response);
431
- } catch (error) {
432
- if (error instanceof DashboardServiceError) {
433
- throw error;
434
- }
435
- throw new DashboardServiceError(
436
- 'Failed to create task',
437
- undefined,
438
- error instanceof Error ? error : undefined
439
- );
440
- }
441
- }
442
-
443
- /**
444
- * Update an existing task
445
- */
446
- export async function updateTask(
447
- taskId: string,
448
- data: Partial<Task>
449
- ): Promise<Task> {
450
- try {
451
- const response = await fetch(`${API_BASE_URL}/tasks/${taskId}`, {
452
- method: 'PATCH',
453
- headers: {
454
- 'Content-Type': 'application/json',
455
- },
456
- body: JSON.stringify(data),
457
- });
458
-
459
- return await handleResponse<Task>(response);
460
- } catch (error) {
461
- if (error instanceof DashboardServiceError) {
462
- throw error;
463
- }
464
- throw new DashboardServiceError(
465
- 'Failed to update task',
466
- undefined,
467
- error instanceof Error ? error : undefined
468
- );
469
- }
470
- }
471
-
472
- /**
473
- * Delete a task
474
- */
475
- export async function deleteTask(taskId: string): Promise<void> {
476
- try {
477
- const response = await fetch(`${API_BASE_URL}/tasks/${taskId}`, {
478
- method: 'DELETE',
479
- headers: {
480
- 'Content-Type': 'application/json',
481
- },
482
- });
483
-
484
- if (!response.ok) {
485
- throw new DashboardServiceError(
486
- `Failed to delete task: ${response.statusText}`,
487
- response.status
488
- );
489
- }
490
- } catch (error) {
491
- if (error instanceof DashboardServiceError) {
492
- throw error;
493
- }
494
- throw new DashboardServiceError(
495
- 'Failed to delete task',
496
- undefined,
497
- error instanceof Error ? error : undefined
498
- );
499
- }
500
- }
501
-
502
- /**
503
- * Invite a team member
504
- */
505
- export async function inviteTeamMember(
506
- userId: string,
507
- email: string,
508
- role: TeamMember['role']
509
- ): Promise<TeamMember> {
510
- try {
511
- const response = await fetch(`${API_BASE_URL}/team/invite`, {
512
- method: 'POST',
513
- headers: {
514
- 'Content-Type': 'application/json',
515
- },
516
- body: JSON.stringify({ userId, email, role }),
517
- });
518
-
519
- return await handleResponse<TeamMember>(response);
520
- } catch (error) {
521
- if (error instanceof DashboardServiceError) {
522
- throw error;
523
- }
524
- throw new DashboardServiceError(
525
- 'Failed to invite team member',
526
- undefined,
527
- error instanceof Error ? error : undefined
528
- );
529
- }
530
- }
531
-
532
- /**
533
- * Remove a team member
534
- */
535
- export async function removeTeamMember(memberId: string): Promise<void> {
536
- try {
537
- const response = await fetch(`${API_BASE_URL}/team/${memberId}`, {
538
- method: 'DELETE',
539
- headers: {
540
- 'Content-Type': 'application/json',
541
- },
542
- });
543
-
544
- if (!response.ok) {
545
- throw new DashboardServiceError(
546
- `Failed to remove team member: ${response.statusText}`,
547
- response.status
548
- );
549
- }
550
- } catch (error) {
551
- if (error instanceof DashboardServiceError) {
552
- throw error;
553
- }
554
- throw new DashboardServiceError(
555
- 'Failed to remove team member',
556
- undefined,
557
- error instanceof Error ? error : undefined
558
- );
559
- }
560
- }
561
-
562
- /**
563
- * Update team member role
564
- */
565
- export async function updateTeamMemberRole(
566
- memberId: string,
567
- role: TeamMember['role']
568
- ): Promise<TeamMember> {
569
- try {
570
- const response = await fetch(`${API_BASE_URL}/team/${memberId}`, {
571
- method: 'PATCH',
572
- headers: {
573
- 'Content-Type': 'application/json',
574
- },
575
- body: JSON.stringify({ role }),
576
- });
577
-
578
- return await handleResponse<TeamMember>(response);
579
- } catch (error) {
580
- if (error instanceof DashboardServiceError) {
581
- throw error;
582
- }
583
- throw new DashboardServiceError(
584
- 'Failed to update team member role',
585
- undefined,
586
- error instanceof Error ? error : undefined
587
- );
588
- }
589
- }