diff --git a/ee/tabby-ui/app/auth/components/signup.tsx b/ee/tabby-ui/app/auth/components/signup.tsx
new file mode 100644
index 0000000..b8eeffa
--- /dev/null
+++ b/ee/tabby-ui/app/auth/components/signup.tsx
@@ -0,0 +1,34 @@
+'use client'
+
+import { graphql } from '@/lib/gql/generates'
+import { UserAuthForm } from './user-auth-form'
+import { useGraphQL } from '@/lib/hooks/use-graphql'
+import { useSearchParams } from 'next/navigation'
+
+export const getIsAdminInitialized = graphql(/* GraphQL */ `
+ query GetIsAdminInitialized {
+ isAdminInitialized
+ }
+`)
+
+export default function Signup() {
+ const { data } = useGraphQL(getIsAdminInitialized)
+ const title = data?.isAdminInitialized
+ ? 'Create an account'
+ : 'Create an admin account'
+
+ const searchParams = useSearchParams()
+ const invitationCode = searchParams.get('invitationCode') || undefined
+
+ return (
+
+
+
{title}
+
+ Fill form below to create your account
+
+
+
+
+ )
+}
diff --git a/ee/tabby-ui/app/auth/components/user-auth-form.tsx b/ee/tabby-ui/app/auth/components/user-auth-form.tsx
new file mode 100644
index 0000000..8f5c228
--- /dev/null
+++ b/ee/tabby-ui/app/auth/components/user-auth-form.tsx
@@ -0,0 +1,146 @@
+'use client'
+
+import * as React from 'react'
+
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm } from 'react-hook-form'
+import * as z from 'zod'
+
+import { cn } from '@/lib/utils'
+import { IconSpinner } from '@/components/ui/icons'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage
+} from '@/components/ui/form'
+import { graphql } from '@/lib/gql/generates'
+import { useGraphQLForm } from '@/lib/tabby-gql-client'
+
+export const registerUser = graphql(/* GraphQL */ `
+ mutation register(
+ $email: String!
+ $password1: String!
+ $password2: String!
+ $invitationCode: String
+ ) {
+ register(
+ email: $email
+ password1: $password1
+ password2: $password2
+ invitationCode: $invitationCode
+ ) {
+ accessToken
+ refreshToken
+ }
+ }
+`)
+
+const formSchema = z.object({
+ email: z.string().email('Invalid email address'),
+ password1: z.string(),
+ password2: z.string(),
+ invitationCode: z.string().optional()
+})
+
+interface UserAuthFormProps extends React.HTMLAttributes {
+ invitationCode?: string
+}
+
+export function UserAuthForm({
+ className,
+ invitationCode,
+ ...props
+}: UserAuthFormProps) {
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: '',
+ password1: '',
+ password2: '',
+ invitationCode
+ }
+ })
+
+ const { isSubmitting } = form.formState
+ const { onSubmit } = useGraphQLForm(registerUser, {
+ onError: (path, message) => form.setError(path as any, { message })
+ })
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/ee/tabby-ui/app/auth/page.tsx b/ee/tabby-ui/app/auth/page.tsx
new file mode 100644
index 0000000..f6a9db4
--- /dev/null
+++ b/ee/tabby-ui/app/auth/page.tsx
@@ -0,0 +1,16 @@
+import { Metadata } from 'next'
+
+import Signup from './components/signup'
+
+export const metadata: Metadata = {
+ title: 'Authentication',
+ description: 'Authentication forms built using the components.'
+}
+
+export default function AuthenticationPage() {
+ return (
+
+
+
+ )
+}
diff --git a/ee/tabby-ui/components/ui/form.tsx b/ee/tabby-ui/components/ui/form.tsx
new file mode 100644
index 0000000..1fd11f4
--- /dev/null
+++ b/ee/tabby-ui/components/ui/form.tsx
@@ -0,0 +1,177 @@
+import * as React from 'react'
+import * as LabelPrimitive from '@radix-ui/react-label'
+import { Slot } from '@radix-ui/react-slot'
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext
+} from 'react-hook-form'
+
+import { cn } from '@/lib/utils'
+import { Label } from '@/components/ui/label'
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+ const name = fieldContext.name || 'root'
+
+ const fieldState = getFieldState(name, formState)
+
+ if (!formState) {
+ throw new Error('useFormField should be used within