tr]:last:border-b-0",
45 | className
46 | )}
47 | data-slot="table-footer"
48 | {...props}
49 | />
50 | );
51 | }
52 |
53 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
54 | return (
55 |
63 | );
64 | }
65 |
66 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
67 | return (
68 | [role=checkbox]]:translate-y-[2px]",
71 | className
72 | )}
73 | data-slot="table-head"
74 | {...props}
75 | />
76 | );
77 | }
78 |
79 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
80 | return (
81 | | [role=checkbox]]:translate-y-[2px]",
84 | className
85 | )}
86 | data-slot="table-cell"
87 | {...props}
88 | />
89 | );
90 | }
91 |
92 | function TableCaption({
93 | className,
94 | ...props
95 | }: React.ComponentProps<"caption">) {
96 | return (
97 |
102 | );
103 | }
104 |
105 | export {
106 | Table,
107 | TableHeader,
108 | TableBody,
109 | TableFooter,
110 | TableHead,
111 | TableRow,
112 | TableCell,
113 | TableCaption,
114 | };
115 |
--------------------------------------------------------------------------------
/apps/server/src/db/migrations/0000_tiresome_rawhide_kid.sql:
--------------------------------------------------------------------------------
1 | CREATE TYPE "public"."tenant_role" AS ENUM('owner', 'admin', 'member');--> statement-breakpoint
2 | CREATE TABLE "account" (
3 | "id" text PRIMARY KEY NOT NULL,
4 | "account_id" text NOT NULL,
5 | "provider_id" text NOT NULL,
6 | "user_id" text NOT NULL,
7 | "access_token" text,
8 | "refresh_token" text,
9 | "id_token" text,
10 | "access_token_expires_at" timestamp,
11 | "refresh_token_expires_at" timestamp,
12 | "scope" text,
13 | "password" text,
14 | "created_at" timestamp NOT NULL,
15 | "updated_at" timestamp NOT NULL
16 | );
17 | --> statement-breakpoint
18 | CREATE TABLE "session" (
19 | "id" text PRIMARY KEY NOT NULL,
20 | "expires_at" timestamp NOT NULL,
21 | "token" text NOT NULL,
22 | "created_at" timestamp NOT NULL,
23 | "updated_at" timestamp NOT NULL,
24 | "ip_address" text,
25 | "user_agent" text,
26 | "user_id" text NOT NULL,
27 | CONSTRAINT "session_token_unique" UNIQUE("token")
28 | );
29 | --> statement-breakpoint
30 | CREATE TABLE "user" (
31 | "id" text PRIMARY KEY NOT NULL,
32 | "name" text NOT NULL,
33 | "email" text NOT NULL,
34 | "email_verified" boolean NOT NULL,
35 | "image" text,
36 | "created_at" timestamp NOT NULL,
37 | "updated_at" timestamp NOT NULL,
38 | CONSTRAINT "user_email_unique" UNIQUE("email")
39 | );
40 | --> statement-breakpoint
41 | CREATE TABLE "verification" (
42 | "id" text PRIMARY KEY NOT NULL,
43 | "identifier" text NOT NULL,
44 | "value" text NOT NULL,
45 | "expires_at" timestamp NOT NULL,
46 | "created_at" timestamp,
47 | "updated_at" timestamp
48 | );
49 | --> statement-breakpoint
50 | CREATE TABLE "tenant" (
51 | "id" text PRIMARY KEY NOT NULL,
52 | "slug" text NOT NULL,
53 | "name" text NOT NULL,
54 | "created_at" timestamp NOT NULL,
55 | "updated_at" timestamp NOT NULL,
56 | CONSTRAINT "tenant_slug_unique" UNIQUE("slug")
57 | );
58 | --> statement-breakpoint
59 | CREATE TABLE "tenant_membership" (
60 | "id" text PRIMARY KEY NOT NULL,
61 | "tenant_id" text NOT NULL,
62 | "user_id" text NOT NULL,
63 | "role" "tenant_role" DEFAULT 'member' NOT NULL,
64 | "created_at" timestamp NOT NULL,
65 | "updated_at" timestamp NOT NULL
66 | );
67 | --> statement-breakpoint
68 | ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
69 | ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
70 | ALTER TABLE "tenant_membership" ADD CONSTRAINT "tenant_membership_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
71 | ALTER TABLE "tenant_membership" ADD CONSTRAINT "tenant_membership_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
72 | CREATE UNIQUE INDEX "tenant_membership_tenant_id_user_id_idx" ON "tenant_membership" USING btree ("tenant_id","user_id");
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "vite build",
7 | "serve": "vite preview",
8 | "dev": "vite dev --port=15001",
9 | "start": "vite",
10 | "wrangler:dev": "wrangler dev --port=15001",
11 | "deploy": "pnpm run build && wrangler deploy",
12 | "cf-typegen": "wrangler types",
13 | "machine-translate": "inlang machine translate --project project.inlang"
14 | },
15 | "dependencies": {
16 | "@daveyplate/better-auth-ui": "^3.2.5",
17 | "@dnd-kit/core": "^6.3.1",
18 | "@dnd-kit/modifiers": "^9.0.0",
19 | "@dnd-kit/sortable": "^10.0.0",
20 | "@dnd-kit/utilities": "^3.2.2",
21 | "@orpc/client": "^1.9.0",
22 | "@orpc/tanstack-query": "^1.9.0",
23 | "@radix-ui/react-alert-dialog": "^1.1.15",
24 | "@radix-ui/react-avatar": "^1.1.10",
25 | "@radix-ui/react-checkbox": "^1.3.3",
26 | "@radix-ui/react-collapsible": "^1.1.12",
27 | "@radix-ui/react-dialog": "^1.1.15",
28 | "@radix-ui/react-dropdown-menu": "^2.1.16",
29 | "@radix-ui/react-label": "^2.1.7",
30 | "@radix-ui/react-scroll-area": "^1.2.10",
31 | "@radix-ui/react-select": "^2.2.6",
32 | "@radix-ui/react-separator": "^1.1.7",
33 | "@radix-ui/react-slot": "^1.2.3",
34 | "@radix-ui/react-tabs": "^1.1.13",
35 | "@radix-ui/react-toggle": "^1.1.10",
36 | "@radix-ui/react-toggle-group": "^1.1.11",
37 | "@radix-ui/react-tooltip": "^1.2.8",
38 | "@tabler/icons-react": "^3.35.0",
39 | "@tailwindcss/postcss": "^4.1.14",
40 | "@tailwindcss/vite": "^4.1.8",
41 | "@tanstack/react-form": "^1.0.5",
42 | "@tanstack/react-query": "^5.85.5",
43 | "@tanstack/react-router": "^1.132.41",
44 | "@tanstack/react-router-with-query": "^1.130.17 ",
45 | "@tanstack/react-start": "^1.132.43",
46 | "@tanstack/react-table": "^8.21.3",
47 | "@tanstack/router-plugin": "^1.132.41",
48 | "better-auth": "^1.3.26",
49 | "class-variance-authority": "^0.7.1",
50 | "clsx": "^2.1.1",
51 | "cmdk": "^1.1.1",
52 | "lucide-react": "^0.525.0",
53 | "next-themes": "^0.4.6",
54 | "postcss": "^8.5.6",
55 | "radix-ui": "^1.4.2",
56 | "react": "19.1.0",
57 | "react-dom": "19.1.0",
58 | "recharts": "2.15.4",
59 | "sonner": "^2.0.7",
60 | "tailwind-merge": "^3.3.1",
61 | "tailwindcss": "^4.1.13",
62 | "tw-animate-css": "^1.2.5",
63 | "vaul": "^1.1.2",
64 | "zod": "^4.1.11"
65 | },
66 | "devDependencies": {
67 | "@cloudflare/vite-plugin": "^1.13.10",
68 | "@inlang/cli": "^3.0.0",
69 | "@inlang/paraglide-js": "2.4.0",
70 | "@tanstack/devtools-vite": "^0.3.3",
71 | "@tanstack/react-devtools": "^0.7.1",
72 | "@tanstack/react-query-devtools": "^5.85.5",
73 | "@tanstack/react-router-devtools": "^1.121.0-alpha.27",
74 | "@testing-library/dom": "^10.4.0",
75 | "@testing-library/react": "^16.2.0",
76 | "@types/react": "~19.1.10",
77 | "@types/react-dom": "^19.0.4",
78 | "@vitejs/plugin-react": "^5.0.1",
79 | "jsdom": "^26.0.0",
80 | "typescript": "^5.7.2",
81 | "vite": "^7.0.2",
82 | "vite-tsconfig-paths": "^5.1.4",
83 | "web-vitals": "^5.0.3",
84 | "wrangler": "^4.42.0"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/apps/native/components/sign-up.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | ActivityIndicator,
4 | KeyboardAvoidingView,
5 | Platform,
6 | Text,
7 | TextInput,
8 | TouchableOpacity,
9 | View,
10 | } from "react-native";
11 | import { authClient } from "@/lib/auth-client";
12 | import { queryClient } from "@/utils/orpc";
13 |
14 | export function SignUp() {
15 | const [name, setName] = useState("");
16 | const [email, setEmail] = useState("");
17 | const [password, setPassword] = useState("");
18 | const [isLoading, setIsLoading] = useState(false);
19 | const [error, setError] = useState(null);
20 |
21 | const handleSignUp = async () => {
22 | setIsLoading(true);
23 | setError(null);
24 |
25 | await authClient.signUp.email(
26 | {
27 | name,
28 | email,
29 | password,
30 | },
31 | {
32 | onError: (error) => {
33 | setError(error.error?.message || "Failed to sign up");
34 | setIsLoading(false);
35 | },
36 | onSuccess: () => {
37 | setName("");
38 | setEmail("");
39 | setPassword("");
40 | queryClient.refetchQueries();
41 | },
42 | onFinished: () => {
43 | setIsLoading(false);
44 | },
45 | }
46 | );
47 | };
48 |
49 | return (
50 |
54 |
55 | Create Account
56 |
57 |
58 | {error && (
59 |
60 | {error}
61 |
62 | )}
63 |
64 |
71 |
72 |
81 |
82 |
90 |
91 |
96 | {isLoading ? (
97 |
98 | ) : (
99 | Sign Up
100 | )}
101 |
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/team-switcher.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronsUpDown, Plus } from "lucide-react";
2 | import * as React from "react";
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuLabel,
8 | DropdownMenuSeparator,
9 | DropdownMenuShortcut,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu";
12 | import {
13 | SidebarMenu,
14 | SidebarMenuButton,
15 | SidebarMenuItem,
16 | useSidebar,
17 | } from "@/components/ui/sidebar";
18 |
19 | type TeamSwitcherProps = {
20 | teams: {
21 | name: string;
22 | logo: React.ElementType;
23 | plan: string;
24 | }[];
25 | };
26 |
27 | export function TeamSwitcher({ teams }: TeamSwitcherProps) {
28 | const { isMobile } = useSidebar();
29 | const [activeTeam, setActiveTeam] = React.useState(teams[0]);
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
40 |
43 |
44 |
45 | {activeTeam.name}
46 |
47 | {activeTeam.plan}
48 |
49 |
50 |
51 |
52 |
58 |
59 | Teams
60 |
61 | {teams.map((team, index) => (
62 | setActiveTeam(team)}
66 | >
67 |
68 |
69 |
70 | {team.name}
71 | ⌘{index + 1}
72 |
73 | ))}
74 |
75 |
76 |
79 | Add team
80 |
81 |
82 |
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/apps/native/app/(drawer)/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { ScrollView, Text, TouchableOpacity, View } from "react-native";
3 | import { Container } from "@/components/container";
4 | import { SignIn } from "@/components/sign-in";
5 | import { SignUp } from "@/components/sign-up";
6 | import { authClient } from "@/lib/auth-client";
7 | import { orpc, queryClient } from "@/utils/orpc";
8 |
9 | export default function Home() {
10 | const healthCheck = useQuery(orpc.healthCheck.queryOptions());
11 | const privateData = useQuery(orpc.privateData.queryOptions());
12 | const { data: session } = authClient.useSession();
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | BETTER T STACK
20 |
21 | {session?.user ? (
22 |
23 |
24 |
25 | Welcome,{" "}
26 | {session.user.name}
27 |
28 |
29 |
30 | {session.user.email}
31 |
32 |
33 | {
36 | authClient.signOut();
37 | queryClient.invalidateQueries();
38 | }}
39 | >
40 | Sign Out
41 |
42 |
43 | ) : null}
44 |
45 | API Status
46 |
47 |
52 |
53 | {healthCheck.isLoading
54 | ? "Checking..."
55 | : healthCheck.data
56 | ? "Connected to API"
57 | : "API Disconnected"}
58 |
59 |
60 |
61 |
62 |
63 | Private Data
64 |
65 | {privateData && (
66 |
67 |
68 | {privateData.data?.message}
69 |
70 |
71 | )}
72 |
73 | {!session?.user && (
74 | <>
75 |
76 |
77 | >
78 | )}
79 |
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/apps/server/scripts/setup-secrets.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Setup Cloudflare Workers Secrets
4 | # This script helps you set up production secrets using wrangler
5 |
6 | set -e
7 |
8 | echo "🔐 Cloudflare Workers Secrets Setup"
9 | echo "===================================="
10 | echo ""
11 | echo "This script will help you set production secrets for your Worker."
12 | echo "Priority order:"
13 | echo " 1. Read from .prod.vars (recommended for production)"
14 | echo " 2. Read from .dev.vars (fallback)"
15 | echo " 3. Enter values manually (interactive)"
16 | echo ""
17 |
18 | # Change to the server directory
19 | cd "$(dirname "$0")/.."
20 |
21 | # Check if wrangler is available
22 | if ! command -v wrangler &> /dev/null; then
23 | echo "❌ Error: wrangler is not installed or not in PATH"
24 | echo " Run: npm install -g wrangler"
25 | exit 1
26 | fi
27 |
28 | # Function to set a secret
29 | set_secret() {
30 | local secret_name=$1
31 | local secret_value=$2
32 |
33 | if [ -z "$secret_value" ]; then
34 | echo "⏭️ Skipping $secret_name (empty value)"
35 | return
36 | fi
37 |
38 | echo "📝 Setting $secret_name..."
39 | echo "$secret_value" | wrangler secret put "$secret_name"
40 | echo "✅ $secret_name set successfully"
41 | echo ""
42 | }
43 |
44 | # Determine which file to use
45 | vars_file=""
46 |
47 | if [ -f ".prod.vars" ]; then
48 | read -p "Read secrets from .prod.vars file? (y/n): " use_prod_vars
49 | if [ "$use_prod_vars" = "y" ] || [ "$use_prod_vars" = "Y" ]; then
50 | vars_file=".prod.vars"
51 | fi
52 | elif [ -f ".dev.vars" ]; then
53 | echo "⚠️ No .prod.vars found. You can create one from .prod.vars.example"
54 | read -p "Read secrets from .dev.vars instead? (y/n): " use_dev_vars
55 | if [ "$use_dev_vars" = "y" ] || [ "$use_dev_vars" = "Y" ]; then
56 | vars_file=".dev.vars"
57 | fi
58 | fi
59 |
60 | if [ -n "$vars_file" ]; then
61 | # Read from vars file
62 | echo ""
63 | echo "📖 Reading secrets from $vars_file..."
64 | echo ""
65 |
66 | # Source the vars file
67 | export $(grep -v '^#' "$vars_file" | xargs)
68 |
69 | # Set each secret
70 | set_secret "DATABASE_URL" "$DATABASE_URL"
71 | set_secret "DATABASE_URL_POOLER" "$DATABASE_URL_POOLER"
72 | set_secret "BETTER_AUTH_SECRET" "$BETTER_AUTH_SECRET"
73 | set_secret "GITHUB_CLIENT_SECRET" "$GITHUB_CLIENT_SECRET"
74 | set_secret "GOOGLE_CLIENT_SECRET" "$GOOGLE_CLIENT_SECRET"
75 |
76 | else
77 | # Manual input
78 | echo ""
79 | echo "📝 Enter secrets manually (press Enter to skip):"
80 | echo ""
81 |
82 | read -p "DATABASE_URL: " DATABASE_URL
83 | set_secret "DATABASE_URL" "$DATABASE_URL"
84 |
85 | read -p "DATABASE_URL_POOLER: " DATABASE_URL_POOLER
86 | set_secret "DATABASE_URL_POOLER" "$DATABASE_URL_POOLER"
87 |
88 | read -p "BETTER_AUTH_SECRET: " BETTER_AUTH_SECRET
89 | set_secret "BETTER_AUTH_SECRET" "$BETTER_AUTH_SECRET"
90 |
91 | read -p "GITHUB_CLIENT_SECRET: " GITHUB_CLIENT_SECRET
92 | set_secret "GITHUB_CLIENT_SECRET" "$GITHUB_CLIENT_SECRET"
93 |
94 | read -p "GOOGLE_CLIENT_SECRET: " GOOGLE_CLIENT_SECRET
95 | set_secret "GOOGLE_CLIENT_SECRET" "$GOOGLE_CLIENT_SECRET"
96 | fi
97 |
98 | echo ""
99 | echo "✨ All secrets have been set successfully!"
100 | echo ""
101 | echo "Next steps:"
102 | echo " 1. Add non-sensitive public IDs to wrangler.jsonc vars:"
103 | echo " - GITHUB_CLIENT_ID"
104 | echo " - GOOGLE_CLIENT_ID"
105 | echo " 2. Deploy your worker: pnpm run deploy"
106 | echo ""
107 |
--------------------------------------------------------------------------------
/apps/web/src/components/command-menu.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "@tanstack/react-router";
2 | import { ArrowRight, ChevronRight, Laptop, Moon, Sun } from "lucide-react";
3 | import React from "react";
4 | import { useSearch } from "@/components/context/search-provider";
5 | import { useTheme } from "@/components/context/theme-provider";
6 | import {
7 | CommandDialog,
8 | CommandEmpty,
9 | CommandGroup,
10 | CommandInput,
11 | CommandItem,
12 | CommandList,
13 | CommandSeparator,
14 | } from "@/components/ui/command";
15 | import { sidebarData } from "./layout/data/sidebar-data";
16 | import { ScrollArea } from "./ui/scroll-area";
17 |
18 | export function CommandMenu() {
19 | const navigate = useNavigate();
20 | const { setTheme } = useTheme();
21 | const { open, setOpen } = useSearch();
22 |
23 | const runCommand = React.useCallback(
24 | (command: () => unknown) => {
25 | setOpen(false);
26 | command();
27 | },
28 | [setOpen]
29 | );
30 |
31 | return (
32 |
33 |
34 |
35 |
36 | No results found.
37 | {sidebarData.navGroups.map((group) => (
38 |
39 | {group.items.map((navItem, i) => {
40 | if (navItem.url) {
41 | return (
42 | {
45 | runCommand(() => navigate({ to: navItem.url }));
46 | }}
47 | value={navItem.title}
48 | >
49 |
52 | {navItem.title}
53 |
54 | );
55 | }
56 |
57 | return navItem.items?.map((subItem, i) => (
58 | {
61 | runCommand(() => navigate({ to: subItem.url }));
62 | }}
63 | value={`${navItem.title}-${subItem.url}`}
64 | >
65 |
68 | {navItem.title} {subItem.title}
69 |
70 | ));
71 | })}
72 |
73 | ))}
74 |
75 |
76 | runCommand(() => setTheme("light"))}>
77 | Light
78 |
79 | runCommand(() => setTheme("dark"))}>
80 |
81 | Dark
82 |
83 | runCommand(() => setTheme("system"))}>
84 |
85 | System
86 |
87 |
88 |
89 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ShipFullStack
2 |
3 | A modern TypeScript stack that combines React, TanStack Start, Hono, ORPC, Expo and more.
4 |
5 | ## Features
6 |
7 | - **TypeScript** - For type safety and improved developer experience
8 | - **TanStack Start** - SSR framework with TanStack Router
9 | - **React Native** - Build mobile apps using React
10 | - **Expo** - Tools for React Native development
11 | - **TailwindCSS** - Utility-first CSS for rapid UI development
12 | - **shadcn/ui** - Reusable UI components
13 | - **Hono** - Lightweight, performant server framework
14 | - **oRPC** - End-to-end type-safe APIs with OpenAPI integration
15 | - **Node.js** - Runtime environment
16 | - **Drizzle** - TypeScript-first ORM
17 | - **PostgreSQL** - Database engine
18 | - **Authentication** - Better-Auth
19 | - **Husky** - Git hooks for code quality
20 | - **Turborepo** - Optimized monorepo build system
21 |
22 | ## Getting Started
23 |
24 | First, install the dependencies:
25 |
26 | ```bash
27 | pnpm install
28 | ```
29 | ## Database Setup
30 |
31 | This project uses PostgreSQL with Drizzle ORM.
32 |
33 | 1. Make sure you have a PostgreSQL database set up.
34 | 2. Update your `apps/server/.env` file with your PostgreSQL connection details.
35 |
36 | 3. Apply the schema to your database:
37 | ```bash
38 | pnpm db:push
39 | ```
40 |
41 |
42 | Then, run the development server:
43 |
44 | ```bash
45 | pnpm dev
46 | ```
47 |
48 | Open [http://localhost:3001](http://localhost:3001) in your browser to see the web application.
49 | Use the Expo Go app to run the mobile application.
50 | The API is running at [http://localhost:3000](http://localhost:3000).
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | ## Project Structure
59 |
60 | ```
61 | ShipFullStack/
62 | ├── apps/
63 | │ ├── web/ # Frontend application (React + TanStack Start)
64 | │ ├── native/ # Mobile application (React Native, Expo)
65 | │ └── server/ # Backend API (Hono, ORPC)
66 | ```
67 |
68 | ## Available Scripts
69 |
70 | - `pnpm dev`: Start all applications in development mode
71 | - `pnpm build`: Build all applications
72 | - `pnpm dev:web`: Start only the web application
73 | - `pnpm dev:server`: Start only the server
74 | - `pnpm check-types`: Check TypeScript types across all apps
75 | - `pnpm dev:native`: Start the React Native/Expo development server
76 | - `pnpm db:push`: Push schema changes to database
77 | - `pnpm db:studio`: Open database studio UI
78 |
79 | ## 🚀 Deployment
80 |
81 | This project supports **automatic deployment** via GitHub Actions. Simply push to the `main` branch and your apps will be automatically deployed to Cloudflare!
82 |
83 | ### Quick Setup
84 |
85 | 1. **Configure Cloudflare Secrets** in GitHub:
86 | - `CLOUDFLARE_API_TOKEN`
87 | - `CLOUDFLARE_ACCOUNT_ID`
88 |
89 | 2. **Push to deploy**:
90 | ```bash
91 | git add .
92 | git commit -m "feat: your changes"
93 | git push origin main
94 | ```
95 |
96 | That's it! Your changes will be automatically deployed. 🎉
97 |
98 | ### Features
99 |
100 | - ✅ **Smart deployment**: Only deploys apps with actual changes
101 | - ✅ **PR preview**: Automatic build checks on pull requests
102 | - ✅ **Manual trigger**: Deploy anytime from GitHub Actions
103 | - ✅ **Deployment summary**: Clear status for each deployment
104 |
105 | ### Documentation
106 |
107 | - [Quick Setup Guide](./.github/SETUP.md)
108 | - [Detailed Deployment Guide](./.github/DEPLOYMENT.md)
109 |
110 | ### Manual Deployment (Alternative)
111 |
112 | If you prefer manual deployment:
113 |
114 | ```bash
115 | # Deploy all applications
116 | pnpm run deploy
117 |
118 | # Deploy only web app
119 | pnpm run deploy:web
120 |
121 | # Deploy only server app
122 | pnpm run deploy:server
123 | ```
124 |
125 | ## Join Discord
126 |
127 | https://discord.gg/KZ6uwbHZG4
128 |
--------------------------------------------------------------------------------
/apps/web/src/components/nav-user.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | IconCreditCard,
3 | IconDotsVertical,
4 | IconLogout,
5 | IconNotification,
6 | IconUserCircle,
7 | } from "@tabler/icons-react";
8 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuGroup,
13 | DropdownMenuItem,
14 | DropdownMenuLabel,
15 | DropdownMenuSeparator,
16 | DropdownMenuTrigger,
17 | } from "@/components/ui/dropdown-menu";
18 | import {
19 | SidebarMenu,
20 | SidebarMenuButton,
21 | SidebarMenuItem,
22 | useSidebar,
23 | } from "@/components/ui/sidebar";
24 | import { useSignOut } from "@/hooks/use-sign-out";
25 |
26 | export function NavUser({
27 | user,
28 | }: {
29 | user: {
30 | name: string;
31 | email: string;
32 | avatar: string;
33 | };
34 | }) {
35 | const { isMobile } = useSidebar();
36 | const { signOut } = useSignOut();
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
47 |
48 |
49 | CN
50 |
51 |
52 | {user.name}
53 |
54 | {user.email}
55 |
56 |
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 |
70 | CN
71 |
72 |
73 | {user.name}
74 |
75 | {user.email}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | Account
85 |
86 |
87 |
88 | Billing
89 |
90 |
91 |
92 | Notifications
93 |
94 |
95 |
96 |
97 |
98 | Log out
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/apps/web/src/components/section-cards.tsx:
--------------------------------------------------------------------------------
1 | import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
2 |
3 | import { Badge } from "@/components/ui/badge";
4 | import {
5 | Card,
6 | CardAction,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 |
13 | export function SectionCards() {
14 | return (
15 |
16 |
17 |
18 | Total Revenue
19 |
20 | $1,250.00
21 |
22 |
23 |
24 |
25 | +12.5%
26 |
27 |
28 |
29 |
30 |
31 | Trending up this month
32 |
33 |
34 | Visitors for the last 6 months
35 |
36 |
37 |
38 |
39 |
40 | New Customers
41 |
42 | 1,234
43 |
44 |
45 |
46 |
47 | -20%
48 |
49 |
50 |
51 |
52 |
53 | Down 20% this period
54 |
55 |
56 | Acquisition needs attention
57 |
58 |
59 |
60 |
61 |
62 | Active Accounts
63 |
64 | 45,678
65 |
66 |
67 |
68 |
69 | +12.5%
70 |
71 |
72 |
73 |
74 |
75 | Strong user retention
76 |
77 | Engagement exceed targets
78 |
79 |
80 |
81 |
82 | Growth Rate
83 |
84 | 4.5%
85 |
86 |
87 |
88 |
89 | +4.5%
90 |
91 |
92 |
93 |
94 |
95 | Steady performance increase
96 |
97 | Meets growth projections
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/apps/web/src/components/context/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import { ScriptOnce } from "@tanstack/react-router";
2 | import { createClientOnlyFn, createIsomorphicFn } from "@tanstack/react-start";
3 | import { createContext, type ReactNode, use, useEffect, useState } from "react";
4 | import { z } from "zod";
5 |
6 | const UserThemeSchema = z.enum(["light", "dark", "system"]).catch("system");
7 | const AppThemeSchema = z.enum(["light", "dark"]).catch("light");
8 |
9 | export type UserTheme = z.infer;
10 | export type AppTheme = z.infer;
11 |
12 | const themeStorageKey = "ui-theme";
13 |
14 | const getStoredUserTheme = createIsomorphicFn()
15 | .server((): UserTheme => "system")
16 | .client((): UserTheme => {
17 | const stored = localStorage.getItem(themeStorageKey);
18 | return UserThemeSchema.parse(stored);
19 | });
20 |
21 | const setStoredTheme = createClientOnlyFn((theme: UserTheme) => {
22 | const validatedTheme = UserThemeSchema.parse(theme);
23 | localStorage.setItem(themeStorageKey, validatedTheme);
24 | });
25 |
26 | const getSystemTheme = createIsomorphicFn()
27 | .server((): AppTheme => "light")
28 | .client(
29 | (): AppTheme =>
30 | window.matchMedia("(prefers-color-scheme: dark)").matches
31 | ? "dark"
32 | : "light"
33 | );
34 |
35 | const handleThemeChange = createClientOnlyFn((userTheme: UserTheme) => {
36 | const validatedTheme = UserThemeSchema.parse(userTheme);
37 |
38 | const root = document.documentElement;
39 | root.classList.remove("light", "dark", "system");
40 |
41 | if (validatedTheme === "system") {
42 | const systemTheme = getSystemTheme();
43 | root.classList.add(systemTheme, "system");
44 | } else {
45 | root.classList.add(validatedTheme);
46 | }
47 | });
48 |
49 | const setupPreferredListener = createClientOnlyFn(() => {
50 | const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
51 | const handler = () => handleThemeChange("system");
52 | mediaQuery.addEventListener("change", handler);
53 | return () => mediaQuery.removeEventListener("change", handler);
54 | });
55 |
56 | const themeScript: string = (() => {
57 | function themeFn() {
58 | try {
59 | const storedTheme = localStorage.getItem("ui-theme") || "system";
60 | const validTheme = ["light", "dark", "system"].includes(storedTheme)
61 | ? storedTheme
62 | : "system";
63 |
64 | if (validTheme === "system") {
65 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
66 | .matches
67 | ? "dark"
68 | : "light";
69 | document.documentElement.classList.add(systemTheme, "system");
70 | } else {
71 | document.documentElement.classList.add(validTheme);
72 | }
73 | } catch {
74 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
75 | .matches
76 | ? "dark"
77 | : "light";
78 | document.documentElement.classList.add(systemTheme, "system");
79 | }
80 | }
81 | return `(${themeFn.toString()})();`;
82 | })();
83 |
84 | type ThemeContextProps = {
85 | userTheme: UserTheme;
86 | appTheme: AppTheme;
87 | setTheme: (theme: UserTheme) => void;
88 | };
89 | const ThemeContext = createContext(undefined);
90 |
91 | type ThemeProviderProps = {
92 | children: ReactNode;
93 | };
94 |
95 | export function ThemeProvider({ children }: ThemeProviderProps) {
96 | const [userTheme, setUserTheme] = useState(getStoredUserTheme);
97 |
98 | useEffect(() => {
99 | if (userTheme !== "system") {
100 | return;
101 | }
102 | return setupPreferredListener();
103 | }, [userTheme]);
104 |
105 | const appTheme = userTheme === "system" ? getSystemTheme() : userTheme;
106 |
107 | const setTheme = (newUserTheme: UserTheme) => {
108 | const validatedTheme = UserThemeSchema.parse(newUserTheme);
109 | setUserTheme(validatedTheme);
110 | setStoredTheme(validatedTheme);
111 | handleThemeChange(validatedTheme);
112 | };
113 |
114 | return (
115 |
116 | {themeScript}
117 | {children}
118 |
119 | );
120 | }
121 |
122 | export const useTheme = () => {
123 | const context = use(ThemeContext);
124 | if (!context) {
125 | throw new Error("useTheme must be used within a ThemeProvider");
126 | }
127 | return context;
128 | };
129 |
--------------------------------------------------------------------------------
/apps/web/src/components/sign-in-form.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "@tanstack/react-form";
2 | import { useNavigate } from "@tanstack/react-router";
3 | import { toast } from "sonner";
4 | import z from "zod";
5 | import { authClient } from "@/lib/auth/auth-client";
6 | import Loader from "./loader";
7 | import { Button } from "./ui/button";
8 | import { Input } from "./ui/input";
9 | import { Label } from "./ui/label";
10 |
11 | export default function SignInForm({
12 | onSwitchToSignUp,
13 | }: {
14 | onSwitchToSignUp: () => void;
15 | }) {
16 | const navigate = useNavigate({
17 | from: "/",
18 | });
19 | const { isPending } = authClient.useSession();
20 |
21 | const form = useForm({
22 | defaultValues: {
23 | email: "",
24 | password: "",
25 | },
26 | onSubmit: async ({ value }) => {
27 | await authClient.signIn.email(
28 | {
29 | email: value.email,
30 | password: value.password,
31 | },
32 | {
33 | onSuccess: () => {
34 | navigate({
35 | to: "/dashboard",
36 | });
37 | toast.success("Sign in successful");
38 | },
39 | onError: (error) => {
40 | toast.error(error.error.message || error.error.statusText);
41 | },
42 | }
43 | );
44 | },
45 | validators: {
46 | onSubmit: z.object({
47 | email: z.email("Invalid email address"),
48 | password: z.string().min(8, "Password must be at least 8 characters"),
49 | }),
50 | },
51 | });
52 |
53 | if (isPending) {
54 | return ;
55 | }
56 |
57 | return (
58 |
59 | Welcome Back
60 |
61 |
116 | {(state) => (
117 |
124 | )}
125 |
126 |
127 |
128 |
129 |
136 |
137 |
138 | );
139 | }
140 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 |
7 | function AlertDialog({
8 | ...props
9 | }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function AlertDialogTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return (
17 |
18 | )
19 | }
20 |
21 | function AlertDialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 | )
27 | }
28 |
29 | function AlertDialogOverlay({
30 | className,
31 | ...props
32 | }: React.ComponentProps) {
33 | return (
34 |
42 | )
43 | }
44 |
45 | function AlertDialogContent({
46 | className,
47 | ...props
48 | }: React.ComponentProps) {
49 | return (
50 |
51 |
52 |
60 |
61 | )
62 | }
63 |
64 | function AlertDialogHeader({
65 | className,
66 | ...props
67 | }: React.ComponentProps<"div">) {
68 | return (
69 |
74 | )
75 | }
76 |
77 | function AlertDialogFooter({
78 | className,
79 | ...props
80 | }: React.ComponentProps<"div">) {
81 | return (
82 |
90 | )
91 | }
92 |
93 | function AlertDialogTitle({
94 | className,
95 | ...props
96 | }: React.ComponentProps) {
97 | return (
98 |
103 | )
104 | }
105 |
106 | function AlertDialogDescription({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
116 | )
117 | }
118 |
119 | function AlertDialogAction({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
128 | )
129 | }
130 |
131 | function AlertDialogCancel({
132 | className,
133 | ...props
134 | }: React.ComponentProps) {
135 | return (
136 |
140 | )
141 | }
142 |
143 | export {
144 | AlertDialog,
145 | AlertDialogPortal,
146 | AlertDialogOverlay,
147 | AlertDialogTrigger,
148 | AlertDialogContent,
149 | AlertDialogHeader,
150 | AlertDialogFooter,
151 | AlertDialogTitle,
152 | AlertDialogDescription,
153 | AlertDialogAction,
154 | AlertDialogCancel,
155 | }
156 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { XIcon } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Dialog({
8 | ...props
9 | }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function DialogTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return
17 | }
18 |
19 | function DialogPortal({
20 | ...props
21 | }: React.ComponentProps) {
22 | return
23 | }
24 |
25 | function DialogClose({
26 | ...props
27 | }: React.ComponentProps) {
28 | return
29 | }
30 |
31 | function DialogOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | )
45 | }
46 |
47 | function DialogContent({
48 | className,
49 | children,
50 | showCloseButton = true,
51 | ...props
52 | }: React.ComponentProps & {
53 | showCloseButton?: boolean
54 | }) {
55 | return (
56 |
57 |
58 |
66 | {children}
67 | {showCloseButton && (
68 |
72 |
73 | Close
74 |
75 | )}
76 |
77 |
78 | )
79 | }
80 |
81 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
82 | return (
83 |
88 | )
89 | }
90 |
91 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
92 | return (
93 |
101 | )
102 | }
103 |
104 | function DialogTitle({
105 | className,
106 | ...props
107 | }: React.ComponentProps) {
108 | return (
109 |
114 | )
115 | }
116 |
117 | function DialogDescription({
118 | className,
119 | ...props
120 | }: React.ComponentProps) {
121 | return (
122 |
127 | )
128 | }
129 |
130 | export {
131 | Dialog,
132 | DialogClose,
133 | DialogContent,
134 | DialogDescription,
135 | DialogFooter,
136 | DialogHeader,
137 | DialogOverlay,
138 | DialogPortal,
139 | DialogTitle,
140 | DialogTrigger,
141 | }
142 |
--------------------------------------------------------------------------------
/apps/web/src/components/layout/nav-user.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronsUpDown, LogOut, Sparkles } from "lucide-react";
2 | import { SignOutDialog } from "@/components/sign-out-dialog";
3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuGroup,
8 | DropdownMenuItem,
9 | DropdownMenuLabel,
10 | DropdownMenuSeparator,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu";
13 | import {
14 | SidebarMenu,
15 | SidebarMenuButton,
16 | SidebarMenuItem,
17 | useSidebar,
18 | } from "@/components/ui/sidebar";
19 | import useDialogState from "@/hooks/use-dialog-state";
20 |
21 | type NavUserProps = {
22 | user: {
23 | name: string;
24 | email: string;
25 | avatar: string;
26 | };
27 | };
28 |
29 | export function NavUser({ user }: NavUserProps) {
30 | const { isMobile } = useSidebar();
31 | const [open, setOpen] = useDialogState();
32 |
33 | return (
34 | <>
35 |
36 |
37 |
38 |
39 |
43 |
44 |
45 | SN
46 |
47 |
48 | {user.name}
49 | {user.email}
50 |
51 |
52 |
53 |
54 |
60 |
61 |
62 |
63 |
64 | SN
65 |
66 |
67 | {user.name}
68 | {user.email}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Upgrade to Pro
77 |
78 |
79 |
80 |
81 |
82 | {/*
83 |
84 | Account
85 | */}
86 |
87 |
88 | {/*
89 |
90 | Billing
91 | */}
92 |
93 |
94 | {/*
95 |
96 | Notifications
97 | */}
98 |
99 |
100 |
101 | setOpen(true)}
103 | variant="destructive"
104 | >
105 |
106 | Sign out
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | >
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
|