diff --git a/apps/backend/package.json b/apps/backend/package.json
new file mode 100644
index 0000000..4943d39
--- /dev/null
+++ b/apps/backend/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@workspace/backend",
+ "license": "MIT",
+ "type": "module",
+ "exports": {
+ "./supabase/types": "./types.ts",
+ "./supabase/utils": "./utils.ts"
+ },
+ "scripts": {
+ "dev": "npx supabase start && npx supabase functions serve"
+ }
+}
diff --git a/apps/backend/supabase/config.toml b/apps/backend/supabase/config.toml
index ccca261..2482f0a 100644
--- a/apps/backend/supabase/config.toml
+++ b/apps/backend/supabase/config.toml
@@ -7,7 +7,7 @@ project_id = "backend"
[api]
enabled = true
# Port to use for the API URL.
-port = 94321
+port = 14321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
@@ -23,9 +23,9 @@ enabled = false
[db]
# Port to use for the local database URL.
-port = 94322
+port = 14322
# Port used by db diff command to initialize the shadow database.
-shadow_port = 94320
+shadow_port = 14320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 17
@@ -33,7 +33,7 @@ major_version = 17
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
-port = 94329
+port = 14329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
diff --git a/apps/backend/supabase/migrations/20251111143326_profiles_and_repositories.sql b/apps/backend/supabase/migrations/20251111143326_profiles_and_repositories.sql
new file mode 100644
index 0000000..366f05b
--- /dev/null
+++ b/apps/backend/supabase/migrations/20251111143326_profiles_and_repositories.sql
@@ -0,0 +1,77 @@
+create or replace function public.handle_new_user()
+returns trigger as $$
+begin
+ insert into public.profiles (id, full_name)
+ values (new.id, new.raw_user_meta_data->>'full_name');
+ return new;
+end;
+$$ language plpgsql security definer;
+
+create trigger on_auth_user_created
+after insert on auth.users
+for each row execute function public.handle_new_user();
+
+create type "public"."profile_repositories_statuses" as enum ('PENDING', 'ACCEPTED');
+
+create table "public"."profile_repositories" (
+ "profile" uuid not null default gen_random_uuid(),
+ "repository" uuid not null default gen_random_uuid(),
+ "status" profile_repositories_statuses not null default 'PENDING'::profile_repositories_statuses,
+ "created_at" timestamp with time zone not null default now()
+);
+
+create policy "Enable users to view their own data only"
+on "public"."profile_repositories"
+as permissive
+for select
+to authenticated
+using ((( SELECT auth.uid() AS uid) = profile));
+
+alter table "public"."profile_repositories" enable row level security;
+
+create table "public"."profiles" (
+ "id" uuid not null default auth.uid(),
+ "full_name" text not null,
+ "created_at" timestamp with time zone not null default now()
+);
+
+alter table "public"."profiles" enable row level security;
+
+create table "public"."repositories" (
+ "id" uuid not null default gen_random_uuid(),
+ "name" text not null,
+ "created_at" timestamp with time zone not null default now()
+);
+
+alter table "public"."repositories" enable row level security;
+
+CREATE UNIQUE INDEX profile_repositories_pkey ON public.profile_repositories USING btree (profile, repository);
+
+CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id);
+
+CREATE UNIQUE INDEX repositories_pkey ON public.repositories USING btree (id);
+
+alter table "public"."profile_repositories" add constraint "profile_repositories_pkey" PRIMARY KEY using index "profile_repositories_pkey";
+
+alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey";
+
+alter table "public"."repositories" add constraint "repositories_pkey" PRIMARY KEY using index "repositories_pkey";
+
+alter table "public"."profile_repositories" add constraint "profile_repositories_profile_fkey" FOREIGN KEY (profile) REFERENCES profiles(id) not valid;
+
+alter table "public"."profile_repositories" validate constraint "profile_repositories_profile_fkey";
+
+alter table "public"."profile_repositories" add constraint "profile_repositories_repository_fkey" FOREIGN KEY (repository) REFERENCES repositories(id) not valid;
+
+alter table "public"."profile_repositories" validate constraint "profile_repositories_repository_fkey";
+
+alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE SET DEFAULT not valid;
+
+alter table "public"."profiles" validate constraint "profiles_id_fkey";
+
+create policy "Enable write for all auth"
+on "public"."repositories"
+as permissive
+for insert
+to authenticated
+with check (true);
\ No newline at end of file
diff --git a/apps/backend/supabase/migrations/20251111160101_create_repository_function.sql b/apps/backend/supabase/migrations/20251111160101_create_repository_function.sql
new file mode 100644
index 0000000..6a86bc2
--- /dev/null
+++ b/apps/backend/supabase/migrations/20251111160101_create_repository_function.sql
@@ -0,0 +1,25 @@
+CREATE OR REPLACE FUNCTION public.create_repository(
+ project_name text
+)
+RETURNS repositories
+LANGUAGE plpgsql
+STRICT
+VOLATILE
+SECURITY DEFINER
+AS $func$
+DECLARE
+ v_repo repositories%ROWTYPE;
+BEGIN
+ -- 1) Create the repository
+ INSERT INTO repositories (name)
+ VALUES (project_name)
+ RETURNING * INTO v_repo;
+
+ -- 2) Link it to the profile
+ INSERT INTO profile_repositories (profile, repository, status)
+ VALUES (auth.uid(), v_repo.id, 'ACCEPTED');
+
+ -- 3) Return the inserted repo row
+ RETURN v_repo;
+END
+$func$;
\ No newline at end of file
diff --git a/apps/backend/supabase/migrations/20251111163017_create_projects_and_secrets.sql b/apps/backend/supabase/migrations/20251111163017_create_projects_and_secrets.sql
new file mode 100644
index 0000000..4130a5c
--- /dev/null
+++ b/apps/backend/supabase/migrations/20251111163017_create_projects_and_secrets.sql
@@ -0,0 +1,41 @@
+create type "public"."project_types" as enum ('SUPABASE', 'VERCEL');
+
+create table "public"."projects" (
+ "id" uuid not null default gen_random_uuid (),
+ "name" text not null,
+ "repository" uuid not null,
+ "url" text null,
+ "type" public.project_types not null,
+ "created_at" timestamp with time zone not null default now(),
+ constraint projects_pkey primary key (id),
+ constraint projects_repository_fkey foreign KEY (repository) references repositories (id)
+) TABLESPACE pg_default;
+
+alter table "public"."projects" enable row level security;
+
+create policy "Enable insert for repo user"
+on "public"."projects"
+as permissive
+for insert
+to authenticated
+with check ((EXISTS ( SELECT 1
+ FROM repositories
+ WHERE (repositories.id = projects.repository))));
+
+create policy "Enabled read for repo member"
+on "public"."projects"
+as permissive
+for select
+to authenticated
+using ((EXISTS ( SELECT 1
+ FROM repositories
+ WHERE (repositories.id = projects.repository))));
+
+create policy "Enable read for users"
+on "public"."repositories"
+as permissive
+for select
+to authenticated
+using ((EXISTS ( SELECT 1
+ FROM profile_repositories
+ WHERE (profile_repositories.repository = repositories.id))));
\ No newline at end of file
diff --git a/apps/frontend/app/page.tsx b/apps/frontend/app/(app)/page.tsx
similarity index 58%
rename from apps/frontend/app/page.tsx
rename to apps/frontend/app/(app)/page.tsx
index 295f8fd..eec8c7b 100644
--- a/apps/frontend/app/page.tsx
+++ b/apps/frontend/app/(app)/page.tsx
@@ -1,6 +1,18 @@
+import { createClient } from "@/utils/supabase/server";
import Image from "next/image";
+import { redirect } from "next/navigation";
+
+export default async function Home() {
+
+ const supabase = await createClient();
+ const {data: user} = await supabase.auth.getUser();
+ console.log('User', user)
+ const {data, error} = await supabase.from('profile_repositories').select('*').eq('profile', user.user?.id);
+
+ if (data?.length === 0){
+ redirect('/wizard');
+ }
-export default function Home() {
return (
@@ -35,29 +47,8 @@ export default function Home() {
diff --git a/apps/frontend/app/(app)/repository/[id]/actions.ts b/apps/frontend/app/(app)/repository/[id]/actions.ts
new file mode 100644
index 0000000..5e2628e
--- /dev/null
+++ b/apps/frontend/app/(app)/repository/[id]/actions.ts
@@ -0,0 +1,22 @@
+'use server'
+
+import { createClient } from "@/utils/supabase/server";
+
+export async function createProjectAction(formData: FormData){
+ const repositoryId = formData.get('repository') as string;
+ const name = formData.get('name') as string;
+ const projectType = formData.get('type') as string;
+
+ console.log(repositoryId, name, projectType);
+
+ const supabase = await createClient();
+ const {data, error} = await supabase.from('projects').insert({
+ name,
+ repository: repositoryId,
+ type: projectType,
+ });
+ if (error){
+ console.error(error.message);
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/apps/frontend/app/(app)/repository/[id]/components/projects-card.tsx b/apps/frontend/app/(app)/repository/[id]/components/projects-card.tsx
new file mode 100644
index 0000000..e868bdd
--- /dev/null
+++ b/apps/frontend/app/(app)/repository/[id]/components/projects-card.tsx
@@ -0,0 +1,42 @@
+import { AddProjectDialog } from "@/components/add-project-dialog";
+import SupabaseIcon from "@/components/supabase-icon";
+import VercelIcon from "@/components/vercel-icon";
+import { createClient } from "@/utils/supabase/server";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/ui/src/components/card";
+import { Folder } from "lucide-react";
+
+
+export async function ProjectsCard({ repositoryId }: { repositoryId: string }) {
+ const supabase = await createClient();
+ const { data: projects, error } = await supabase.from('projects').select('*').eq('repository', repositoryId);
+ if (error){
+ console.error(error.message);
+ throw error;
+ }
+
+ return (
+
+
+
+
+ {projects.length} project{projects.length !== 1 ? 's' : ''} in this repository
+
+
+
+
+ {projects.map((project) => (
+
+ {project.type === 'SUPABASE' ? : }
+ {project.name}
+
+ ))}
+
+
+
+ );
+}
+
diff --git a/apps/frontend/app/(app)/repository/[id]/components/users-card.tsx b/apps/frontend/app/(app)/repository/[id]/components/users-card.tsx
new file mode 100644
index 0000000..aaea92e
--- /dev/null
+++ b/apps/frontend/app/(app)/repository/[id]/components/users-card.tsx
@@ -0,0 +1,49 @@
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/ui/src/components/card";
+import { Users } from "lucide-react";
+
+// Mock data - replace with actual data when available
+const mockUsers = [
+ { id: '1', name: 'John Doe', email: 'john@example.com' },
+ { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
+ { id: '3', name: 'Bob Johnson', email: 'bob@example.com' },
+];
+
+export async function UsersCard({ repositoryId }: { repositoryId: string }) {
+ // TODO: Fetch users from database
+ // const supabase = await createClient();
+ // const { data: users } = await supabase.from('repository_users').select('*, users(*)').eq('repository_id', repositoryId);
+
+ const users = mockUsers;
+
+ return (
+
+
+
+
+ Users
+
+
+ {users.length} user{users.length !== 1 ? 's' : ''} with access
+
+
+
+
+ {users.map((user) => (
+
+
+
+ {user.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
{user.name}
+
{user.email}
+
+
+ ))}
+
+
+
+ );
+}
+
diff --git a/apps/frontend/app/(app)/repository/[id]/components/variables-card.tsx b/apps/frontend/app/(app)/repository/[id]/components/variables-card.tsx
new file mode 100644
index 0000000..ced343e
--- /dev/null
+++ b/apps/frontend/app/(app)/repository/[id]/components/variables-card.tsx
@@ -0,0 +1,50 @@
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/ui/src/components/card";
+import { Key } from "lucide-react";
+
+// Mock data - replace with actual data when available
+const mockVariables = [
+ { id: '1', key: 'DATABASE_URL', value: 'postgresql://...' },
+ { id: '2', key: 'API_KEY', value: 'sk-...' },
+ { id: '3', key: 'ENVIRONMENT', value: 'production' },
+ { id: '4', key: 'DATABASE_URL', value: 'postgresql://...' },
+ { id: '5', key: 'API_KEY', value: 'sk-...' },
+ { id: '6', key: 'ENVIRONMENT', value: 'production' },
+];
+
+export async function VariablesCard({ repositoryId }: { repositoryId: string }) {
+ // TODO: Fetch variables from database
+ // const supabase = await createClient();
+ // const { data: variables } = await supabase.from('variables').select('*').eq('repository_id', repositoryId);
+
+ const variables = mockVariables;
+
+ return (
+
+
+
+
+ Variables
+
+
+ {variables.length} environment variable{variables.length !== 1 ? 's' : ''}
+
+
+
+
+ {variables.map((variable) => (
+
+
+
+ {variable.key}
+
+
+ {variable.value}
+
+
+ ))}
+
+
+
+ );
+}
+
diff --git a/apps/frontend/app/(app)/repository/[id]/page.tsx b/apps/frontend/app/(app)/repository/[id]/page.tsx
new file mode 100644
index 0000000..6910b8f
--- /dev/null
+++ b/apps/frontend/app/(app)/repository/[id]/page.tsx
@@ -0,0 +1,45 @@
+'use server'
+import { createClient } from "@/utils/supabase/server";
+import { notFound } from "next/navigation";
+import { Suspense } from "react";
+import { ProjectsCard } from "./components/projects-card";
+import { VariablesCard } from "./components/variables-card";
+import { UsersCard } from "./components/users-card";
+
+export default async function RepositoryPage({params}: {params: Promise<{id: string}>}){
+ const supabase = await createClient();
+ const {id} = await params;
+ const {data, error} = await supabase.from('repositories').select('*').eq('id', id).single();
+ if (error){
+ console.error(error.message);
+ throw new Error(error.message);
+ }
+ if (!data){
+ notFound();
+ }
+
+ const repository = data;
+
+ return (
+
+
+
{repository.name}
+
Repository details and configuration
+
+
+
+
}>
+
+
+
+
}>
+
+
+
+
}>
+
+
+
+
+ );
+}
diff --git a/apps/frontend/app/(app)/wizard/actions.ts b/apps/frontend/app/(app)/wizard/actions.ts
new file mode 100644
index 0000000..daeaf5f
--- /dev/null
+++ b/apps/frontend/app/(app)/wizard/actions.ts
@@ -0,0 +1,22 @@
+'use server'
+
+import { createClient } from "@/utils/supabase/server";
+import { redirect } from "next/navigation";
+
+export async function createRepoAction(formData: FormData){
+ const name = formData.get('name') as string;
+ console.log(name);
+
+ const supabase = await createClient();
+
+ const {data, error} = await supabase.rpc('create_repository', {
+ project_name: name,
+ });
+
+ if (error){
+ console.error(error);
+ throw new Error(error.message);
+ }
+
+ redirect(`/repository/${data.id}`);
+}
\ No newline at end of file
diff --git a/apps/frontend/app/(app)/wizard/page.tsx b/apps/frontend/app/(app)/wizard/page.tsx
new file mode 100644
index 0000000..dc20066
--- /dev/null
+++ b/apps/frontend/app/(app)/wizard/page.tsx
@@ -0,0 +1,30 @@
+import { Input } from "@repo/ui/src/components/input";
+import { Label } from "@repo/ui/src/components/label";
+import { Card } from "@repo/ui/src/components/card";
+import { SubmitButton } from "@/components/submit-button";
+import { createRepoAction } from "./actions";
+
+export default function WizardPage(){
+ return
+
+
+
+
+}
\ No newline at end of file
diff --git a/apps/frontend/app/(auth)/login/actions.ts b/apps/frontend/app/(auth)/login/actions.ts
new file mode 100644
index 0000000..1ded797
--- /dev/null
+++ b/apps/frontend/app/(auth)/login/actions.ts
@@ -0,0 +1,29 @@
+'use server'
+
+import { createClient } from "@/utils/supabase/server";
+import { redirect } from "next/navigation";
+
+
+export async function loginAction(formData: FormData) {
+ const email = formData.get("email") as string;
+ const password = formData.get("password") as string;
+
+ console.log(email, password);
+
+ if (!email || !password){
+ throw new Error("Missing required fields");
+ }
+
+ const supabase = await createClient();
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (error){
+ console.error(error);
+ throw new Error(error.message);
+ }
+
+ redirect("/");
+}
\ No newline at end of file
diff --git a/apps/frontend/app/(auth)/login/components/login-form.tsx b/apps/frontend/app/(auth)/login/components/login-form.tsx
new file mode 100644
index 0000000..6c1b5f5
--- /dev/null
+++ b/apps/frontend/app/(auth)/login/components/login-form.tsx
@@ -0,0 +1,65 @@
+import { cn } from "@workspace/ui/lib/utils"
+import { Button } from "@workspace/ui/components/button"
+import {
+ Field,
+ FieldDescription,
+ FieldGroup,
+ FieldLabel,
+ FieldSeparator,
+ } from "@workspace/ui/components/field"
+import { Input } from "@workspace/ui/components/input"
+import Link from "next/link"
+import { loginAction } from "../actions"
+export function LoginForm({
+ className,
+ ...props
+}: React.ComponentProps<"form">) {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/apps/frontend/app/(auth)/login/page.tsx b/apps/frontend/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..bcb1218
--- /dev/null
+++ b/apps/frontend/app/(auth)/login/page.tsx
@@ -0,0 +1,32 @@
+import { GalleryVerticalEnd } from "lucide-react"
+import { LoginForm } from "./components/login-form"
+
+
+export default function SignupPage() {
+ return (
+
+
+
+

+
+
+ )
+}
\ No newline at end of file
diff --git a/apps/frontend/app/(auth)/signup/actions/actions.ts b/apps/frontend/app/(auth)/signup/actions/actions.ts
new file mode 100644
index 0000000..3b3c7ca
--- /dev/null
+++ b/apps/frontend/app/(auth)/signup/actions/actions.ts
@@ -0,0 +1,59 @@
+'use server'
+import { createClient } from "@/utils/supabase/server";
+import { redirect } from "next/navigation";
+
+export async function loginAction(formData: FormData) {
+ const email = formData.get("email") as string;
+ const password = formData.get("password") as string;
+
+ console.log(email, password);
+
+ if (!email || !password){
+ throw new Error("Missing required fields");
+ }
+
+ const supabase = await createClient();
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (error){
+ console.error(error);
+ throw new Error(error.message);
+ }
+
+ redirect("/");
+}
+
+export async function signupAction(formData: FormData) {
+ const name = formData.get("name") as string;
+ const email = formData.get("email") as string;
+ const password = formData.get("password") as string;
+
+
+ console.log(name, email, password);
+
+ if (!name || !email || !password){
+
+ throw new Error("Missing required fields");
+ }
+
+ const supabase = await createClient();
+ const { data, error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ data: {
+ full_name: name,
+ },
+ },
+ });
+
+ if (error){
+ console.error(error);
+ throw new Error(error.message);
+ }
+
+ redirect("/");
+}
\ No newline at end of file
diff --git a/apps/frontend/app/(auth)/signup/components/signup-form.tsx b/apps/frontend/app/(auth)/signup/components/signup-form.tsx
new file mode 100644
index 0000000..1632c77
--- /dev/null
+++ b/apps/frontend/app/(auth)/signup/components/signup-form.tsx
@@ -0,0 +1,74 @@
+import { cn } from "@workspace/ui/lib/utils"
+
+import { Button } from "@workspace/ui/components/button"
+import {
+ Field,
+ FieldDescription,
+ FieldGroup,
+ FieldLabel,
+ FieldSeparator,
+} from "@workspace/ui/components/field"
+
+import { Input } from "@workspace/ui/components/input"
+import Link from "next/link"
+import { signupAction } from "../actions/actions"
+
+export function SignupForm({
+ className,
+ ...props
+}: React.ComponentProps<"form">) {
+ return (
+
+ )
+}
diff --git a/apps/frontend/app/(auth)/signup/page.tsx b/apps/frontend/app/(auth)/signup/page.tsx
new file mode 100644
index 0000000..67e94be
--- /dev/null
+++ b/apps/frontend/app/(auth)/signup/page.tsx
@@ -0,0 +1,31 @@
+import { GalleryVerticalEnd } from "lucide-react"
+import { SignupForm } from "../signup/components/signup-form"
+
+export default function SignupPage() {
+ return (
+
+
+
+

+
+
+ )
+}
\ No newline at end of file
diff --git a/apps/frontend/app/login/page.tsx b/apps/frontend/app/login/page.tsx
deleted file mode 100644
index 19f8dd7..0000000
--- a/apps/frontend/app/login/page.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Button } from "@workspace/ui/components/button"
-
-import Link from "next/link";
-
-export default function LoginPage() {
- return (
-
-
Login
- Link to open
-
-
- )
-}
\ No newline at end of file
diff --git a/apps/frontend/components/add-project-dialog.tsx b/apps/frontend/components/add-project-dialog.tsx
new file mode 100644
index 0000000..328f925
--- /dev/null
+++ b/apps/frontend/components/add-project-dialog.tsx
@@ -0,0 +1,114 @@
+'use client'
+
+import { Button } from "@repo/ui/src/components/button";
+import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@repo/ui/src/components/dialog";
+import { Input } from "@repo/ui/src/components/input";
+import { Label } from "@repo/ui/src/components/label";
+import { PlusIcon } from "lucide-react";
+import { SubmitButton } from "./submit-button";
+import { createProjectAction } from "@/app/(app)/repository/[id]/actions";
+import { ProjectTypePicker } from "./project-type-picker";
+
+import { useForm } from "@tanstack/react-form"
+
+import * as z from "zod"
+import { Field, FieldError, FieldGroup, FieldLabel } from "@repo/ui/src/components/field";
+import { Spinner } from "@repo/ui/src/components/spinner";
+
+
+const formSchema = z.object({
+ name: z
+ .string()
+ .min(3, "Name of the project must be at least 5 characters.")
+ .max(32, "The name of the project must be at most 32 characters."),
+ type: z.enum(["SUPABASE", "VERCEL"]),
+ })
+
+export function AddProjectDialog({repositoryId,}: {repositoryId: string,}) {
+
+ const form = useForm({
+ defaultValues: {
+ name: "",
+ type: "",
+ },
+ validators: {
+ onSubmit: formSchema,
+ onBlur: formSchema,
+ },
+ onSubmit: async ({ value }) => {
+ const formData = new FormData();
+ formData.append('name', value.name);
+ formData.append('type', value.type);
+ formData.append('repository', repositoryId);
+ await createProjectAction(formData)
+ }
+ })
+
+ return
+}
\ No newline at end of file
diff --git a/apps/frontend/components/project-type-picker.tsx b/apps/frontend/components/project-type-picker.tsx
new file mode 100644
index 0000000..f860fa1
--- /dev/null
+++ b/apps/frontend/components/project-type-picker.tsx
@@ -0,0 +1,82 @@
+"use client"
+
+import * as React from "react"
+import { Check, ChevronsUpDown } from "lucide-react"
+
+import { cn } from "@repo/ui/src/lib/utils"
+import { Button } from "@repo/ui/src/components/button"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@repo/ui/src/components/command"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@repo/ui/src/components/popover"
+
+const frameworks = [
+ {
+ value: "SUPABASE",
+ label: "Supabase",
+ },
+ {
+ value: "VERCEL",
+ label: "Vercel",
+ }
+]
+
+export function ProjectTypePicker({defaultValue, onChange}: {defaultValue: "VERCEL" | "SUPABASE", onChange: (value: "VERCEL" | "SUPABASE") => void}) {
+ const [open, setOpen] = React.useState(false)
+ const [value, setValue] = React.useState<"VERCEL" | "SUPABASE">(defaultValue)
+
+ return (
+
+
+
+
+
+
+
+
+ No framework found.
+
+ {frameworks.map((framework) => (
+ {
+ setValue(currentValue === value ? "SUPABASE" : currentValue as "VERCEL" | "SUPABASE")
+ setOpen(false)
+ }}
+ >
+ {framework.label}
+
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/apps/frontend/components/submit-button.tsx b/apps/frontend/components/submit-button.tsx
new file mode 100644
index 0000000..b847a15
--- /dev/null
+++ b/apps/frontend/components/submit-button.tsx
@@ -0,0 +1,21 @@
+'use client'
+
+import { useFormStatus } from 'react-dom'
+
+import { Button } from '@repo/ui/src/components/button'
+import { Spinner } from '@repo/ui/src/components/spinner'
+
+export function SubmitButton({
+ text,
+}: {
+ text: string
+}) {
+ const { pending } = useFormStatus()
+
+ return (
+
+ )
+}
diff --git a/apps/frontend/components/supabase-icon.tsx b/apps/frontend/components/supabase-icon.tsx
new file mode 100644
index 0000000..34f9dfe
--- /dev/null
+++ b/apps/frontend/components/supabase-icon.tsx
@@ -0,0 +1,8 @@
+
+export default function SupabaseIcon(){
+ return
+}
\ No newline at end of file
diff --git a/apps/frontend/components/vercel-icon.tsx b/apps/frontend/components/vercel-icon.tsx
new file mode 100644
index 0000000..703fe50
--- /dev/null
+++ b/apps/frontend/components/vercel-icon.tsx
@@ -0,0 +1,9 @@
+
+
+export default function VercelIcon(){
+ return
+}
\ No newline at end of file
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 11cae14..833cf36 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -9,9 +9,10 @@
"lint": "eslint"
},
"dependencies": {
- "@workspace/ui": "workspace:*",
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.81.0",
+ "@tanstack/react-form": "^1.23.8",
+ "@workspace/ui": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.553.0",
@@ -19,15 +20,16 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "^3.4.0",
- "tw-animate-css": "^1.4.0"
+ "tw-animate-css": "^1.4.0",
+ "zod": "^3.25.76"
},
"devDependencies": {
- "@workspace/eslint-config": "workspace:*",
- "@workspace/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@workspace/eslint-config": "workspace:*",
+ "@workspace/typescript-config": "workspace:*",
"eslint": "^9",
"eslint-config-next": "16.0.1",
"tailwindcss": "^4",
diff --git a/apps/frontend/middleware.ts b/apps/frontend/proxy.ts
similarity index 74%
rename from apps/frontend/middleware.ts
rename to apps/frontend/proxy.ts
index c2a004d..9928eeb 100644
--- a/apps/frontend/middleware.ts
+++ b/apps/frontend/proxy.ts
@@ -1,6 +1,7 @@
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
-export async function middleware(request: NextRequest) {
+
+export async function proxy(request: NextRequest) {
// update user's auth session
return await updateSession(request)
}
@@ -13,6 +14,6 @@ export const config = {
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
- '/((?!_next/static|_next/image|favicon.ico|login(?:/.*)?|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
+ '/((?!_next/static|_next/image|favicon.ico|login(?:/.*)?|signup(?:/.*)?|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
\ No newline at end of file
diff --git a/apps/frontend/utils/supabase/middleware.ts b/apps/frontend/utils/supabase/middleware.ts
index 42c9c91..1671958 100644
--- a/apps/frontend/utils/supabase/middleware.ts
+++ b/apps/frontend/utils/supabase/middleware.ts
@@ -25,6 +25,9 @@ export async function updateSession(request: NextRequest) {
}
)
// refreshing the auth token
- await supabase.auth.getUser()
+ const { error } = await supabase.auth.getUser()
+ if (error){
+ return NextResponse.redirect(new URL('/login', request.url))
+ }
return supabaseResponse
}
\ No newline at end of file
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 944ae93..fca3f40 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -7,12 +7,17 @@
"lint": "eslint . --max-warnings 0"
},
"dependencies": {
- "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
"lucide-react": "^0.475.0",
"next-themes": "^0.4.6",
- "react": "^19.1.1",
+ "react": "^19.2.0",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.6",
@@ -22,7 +27,7 @@
"@tailwindcss/postcss": "^4.1.11",
"@turbo/gen": "^2.5.5",
"@types/node": "^20.19.9",
- "@types/react": "^19.1.9",
+ "@types/react": "^19.2.2",
"@types/react-dom": "^19.1.7",
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx
new file mode 100644
index 0000000..2e7392a
--- /dev/null
+++ b/packages/ui/src/components/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@workspace/ui/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/packages/ui/src/components/command.tsx b/packages/ui/src/components/command.tsx
new file mode 100644
index 0000000..369c620
--- /dev/null
+++ b/packages/ui/src/components/command.tsx
@@ -0,0 +1,184 @@
+"use client"
+
+import * as React from "react"
+import { Command as CommandPrimitive } from "cmdk"
+import { SearchIcon } from "lucide-react"
+
+import { cn } from "@workspace/ui/lib/utils"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@workspace/ui/components/dialog"
+
+function Command({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandDialog({
+ title = "Command Palette",
+ description = "Search for a command to run...",
+ children,
+ className,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ title?: string
+ description?: string
+ className?: string
+ showCloseButton?: boolean
+}) {
+ return (
+
+ )
+}
+
+function CommandInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function CommandList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandEmpty({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
new file mode 100644
index 0000000..b905322
--- /dev/null
+++ b/packages/ui/src/components/dialog.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@workspace/ui/lib/utils"
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/packages/ui/src/components/field.tsx b/packages/ui/src/components/field.tsx
new file mode 100644
index 0000000..36cb298
--- /dev/null
+++ b/packages/ui/src/components/field.tsx
@@ -0,0 +1,248 @@
+"use client"
+
+import { useMemo } from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@workspace/ui/lib/utils"
+import { Label } from "@workspace/ui/components/label"
+import { Separator } from "@workspace/ui/components/separator"
+
+function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
+ return (
+