diff --git a/frontend/app/(drawer)/_layout.tsx b/frontend/app/(drawer)/_layout.tsx
index fd06a93..0bea61e 100644
--- a/frontend/app/(drawer)/_layout.tsx
+++ b/frontend/app/(drawer)/_layout.tsx
@@ -55,6 +55,18 @@ export default function Layout() {
             ),
           }}
         />
+        <Drawer.Screen
+          name="team"
+          options={
+            {
+              title: "Team",
+              hidden: !teamId,
+              drawerIcon: ({ size, color }) => (
+                <Icons name="account-group" size={size} color={color} />
+              ),
+            } as DrawerNavigationOptions
+          }
+        />
       </Drawer>
     </GestureHandlerRootView>
   );
diff --git a/frontend/app/(drawer)/team.tsx b/frontend/app/(drawer)/team.tsx
new file mode 100644
index 0000000..7a859d9
--- /dev/null
+++ b/frontend/app/(drawer)/team.tsx
@@ -0,0 +1,3 @@
+import TeamPage from "@/pages/team/page";
+
+export default TeamPage;
diff --git a/frontend/components/containers/server-stats-bar.tsx b/frontend/components/containers/server-stats-bar.tsx
index 923503d..e6a64bc 100644
--- a/frontend/components/containers/server-stats-bar.tsx
+++ b/frontend/components/containers/server-stats-bar.tsx
@@ -56,27 +56,33 @@ const ServerStatsBar = ({ url }: Props) => {
     <XStack gap="$1" p="$2" alignItems="center">
       <XStack gap="$1" alignItems="center" minWidth={48}>
         <Icons name="desktop-tower" size={16} />
-        <Text fontSize="$2">{Math.round(cpu)}%</Text>
+        <Text fontSize="$2" aria-label="CPU">
+          {Math.round(cpu)}%
+        </Text>
       </XStack>
 
       <Separator vertical h="100%" mx="$2" borderColor="$color" />
       <Icons name="memory" size={16} />
-      <Text fontSize="$2">
+      <Text fontSize="$2" aria-label="Memory">
         {memory.used} MB / {memory.total} MB (
         {Math.round((memory.used / memory.total) * 100) || 0}%)
       </Text>
 
       <Separator vertical h="100%" mx="$2" borderColor="$color" />
       <Icons name="harddisk" size={16} />
-      <Text fontSize="$2">
+      <Text fontSize="$2" aria-label="Disk">
         {disk.used} / {disk.total} ({disk.percent})
       </Text>
 
       <Separator vertical h="100%" mx="$2" borderColor="$color" />
       <Icons name="download" size={16} />
-      <Text fontSize="$2">{network.rx} MB</Text>
+      <Text fontSize="$2" aria-label="Network Received">
+        {network.rx} MB
+      </Text>
       <Icons name="upload" size={16} />
-      <Text fontSize="$2">{network.tx} MB</Text>
+      <Text fontSize="$2" aria-label="Network Sent">
+        {network.tx} MB
+      </Text>
     </XStack>
   );
 };
diff --git a/frontend/components/containers/user-menu-button.tsx b/frontend/components/containers/user-menu-button.tsx
index 51d6569..965def1 100644
--- a/frontend/components/containers/user-menu-button.tsx
+++ b/frontend/components/containers/user-menu-button.tsx
@@ -13,6 +13,7 @@ import MenuButton from "../ui/menu-button";
 import Icons from "../ui/icons";
 import { logout, setTeam, useTeamId } from "@/stores/auth";
 import { useUser } from "@/hooks/useUser";
+import TeamForm, { teamFormModal } from "@/pages/team/components/team-form";
 
 const UserMenuButton = () => {
   const user = useUser();
@@ -20,43 +21,46 @@ const UserMenuButton = () => {
   const team = user?.teams?.find((t: any) => t.id === teamId);
 
   return (
-    <MenuButton
-      size="$1"
-      placement="bottom-end"
-      width={213}
-      trigger={
-        <Button
-          bg="$colorTransparent"
-          justifyContent="flex-start"
-          p={0}
-          gap="$1"
-        >
-          <Avatar circular size="$3">
-            <Avatar.Fallback bg="$blue4" />
-          </Avatar>
-          <View flex={1} style={{ textAlign: "left" }}>
-            <Text numberOfLines={1}>{user?.name}</Text>
-            <Text numberOfLines={1} fontWeight="600" mt="$1.5">
-              {team ? `${team.icon} ${team.name}` : "Personal"}
-            </Text>
-          </View>
-          <Icons name="chevron-down" size={16} />
-        </Button>
-      }
-    >
-      <TeamsMenu />
-      <MenuButton.Item
-        onPress={() => console.log("logout")}
-        icon={<Icons name="account" size={16} />}
-        title="Account"
-      />
-      <Separator w="100%" />
-      <MenuButton.Item
-        onPress={() => logout()}
-        icon={<Icons name="logout" size={16} />}
-        title="Logout"
-      />
-    </MenuButton>
+    <>
+      <MenuButton
+        size="$1"
+        placement="bottom-end"
+        width={213}
+        trigger={
+          <Button
+            bg="$colorTransparent"
+            justifyContent="flex-start"
+            p={0}
+            gap="$1"
+          >
+            <Avatar circular size="$3">
+              <Avatar.Fallback bg="$blue4" />
+            </Avatar>
+            <View flex={1} style={{ textAlign: "left" }}>
+              <Text numberOfLines={1}>{user?.name}</Text>
+              <Text numberOfLines={1} fontWeight="600" mt="$1.5">
+                {team ? `${team.icon} ${team.name}` : "Personal"}
+              </Text>
+            </View>
+            <Icons name="chevron-down" size={16} />
+          </Button>
+        }
+      >
+        <TeamsMenu />
+        <MenuButton.Item
+          onPress={() => console.log("logout")}
+          icon={<Icons name="account" size={16} />}
+          title="Account"
+        />
+        <Separator w="100%" />
+        <MenuButton.Item
+          onPress={() => logout()}
+          icon={<Icons name="logout" size={16} />}
+          title="Logout"
+        />
+      </MenuButton>
+      <TeamForm />
+    </>
   );
 };
 
@@ -107,6 +111,7 @@ const TeamsMenu = () => {
       <MenuButton.Item
         icon={<Icons name="plus" size={16} />}
         title="Create Team"
+        onPress={() => teamFormModal.onOpen({ icon: "🍃", name: "" })}
       />
     </MenuButton>
   );
diff --git a/frontend/components/ui/menu-button.tsx b/frontend/components/ui/menu-button.tsx
index 09deef9..55a6896 100644
--- a/frontend/components/ui/menu-button.tsx
+++ b/frontend/components/ui/menu-button.tsx
@@ -21,7 +21,7 @@ const MenuButtonFrame = ({
   ...props
 }: MenuButtonProps) => {
   return (
-    <Popover {...props}>
+    <Popover size="$1" {...props}>
       <Popover.Trigger asChild={asChild}>{trigger}</Popover.Trigger>
 
       <Popover.Content
diff --git a/frontend/components/ui/modal.tsx b/frontend/components/ui/modal.tsx
index 4ef3c18..415b0c2 100644
--- a/frontend/components/ui/modal.tsx
+++ b/frontend/components/ui/modal.tsx
@@ -9,6 +9,7 @@ type ModalProps = {
   description?: string;
   children?: React.ReactNode;
   width?: number | string;
+  maxHeight?: number | string;
 };
 
 const Modal = ({
@@ -17,6 +18,7 @@ const Modal = ({
   title,
   description,
   width = 512,
+  maxHeight = 600,
 }: ModalProps) => {
   const { open, onOpenChange } = disclosure.use();
 
@@ -64,7 +66,7 @@ const Modal = ({
           width="90%"
           maxWidth={width}
           height="90%"
-          maxHeight={600}
+          maxHeight={maxHeight}
         >
           <View p="$4">
             <Dialog.Title>{title}</Dialog.Title>
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts
index 7386624..ceabdc1 100644
--- a/frontend/lib/api.ts
+++ b/frontend/lib/api.ts
@@ -1,5 +1,5 @@
 import { getCurrentServer } from "@/stores/app";
-import authStore from "@/stores/auth";
+import authStore, { logout } from "@/stores/auth";
 import { ofetch } from "ofetch";
 
 const api = ofetch.create({
@@ -23,7 +23,7 @@ const api = ofetch.create({
   },
   onResponseError: (error) => {
     if (error.response.status === 401 && !!authStore.getState().token) {
-      authStore.setState({ token: null });
+      logout();
       throw new Error("Unauthorized");
     }
 
diff --git a/frontend/pages/hosts/components/host-list.tsx b/frontend/pages/hosts/components/host-list.tsx
index f44a09c..98d0f3d 100644
--- a/frontend/pages/hosts/components/host-list.tsx
+++ b/frontend/pages/hosts/components/host-list.tsx
@@ -1,7 +1,5 @@
 import { View, Text, Spinner } from "tamagui";
 import React, { useMemo, useState } from "react";
-import { useQuery } from "@tanstack/react-query";
-import api from "@/lib/api";
 import { useNavigation } from "expo-router";
 import SearchInput from "@/components/ui/search-input";
 import { useTermSession } from "@/stores/terminal-sessions";
diff --git a/frontend/pages/hosts/page.tsx b/frontend/pages/hosts/page.tsx
index 568bdca..1c3a3ea 100644
--- a/frontend/pages/hosts/page.tsx
+++ b/frontend/pages/hosts/page.tsx
@@ -6,22 +6,21 @@ import HostForm, { hostFormModal } from "./components/form";
 import Icons from "@/components/ui/icons";
 import { initialValues } from "./schema/form";
 import KeyForm from "../keychains/components/form";
+import { useUser } from "@/hooks/useUser";
+import { useTeamId } from "@/stores/auth";
 
 export default function HostsPage() {
+  const teamId = useTeamId();
+  const user = useUser();
+
   return (
     <>
       <Drawer.Screen
         options={{
-          headerRight: () => (
-            <Button
-              bg="$colorTransparent"
-              icon={<Icons name="plus" size={24} />}
-              onPress={() => hostFormModal.onOpen(initialValues)}
-              $gtSm={{ mr: "$3" }}
-            >
-              New
-            </Button>
-          ),
+          headerRight:
+            !teamId || user?.teamCanWrite(teamId)
+              ? () => <AddButton />
+              : undefined,
         }}
       />
 
@@ -31,3 +30,14 @@ export default function HostsPage() {
     </>
   );
 }
+
+const AddButton = () => (
+  <Button
+    bg="$colorTransparent"
+    icon={<Icons name="plus" size={24} />}
+    onPress={() => hostFormModal.onOpen(initialValues)}
+    $gtSm={{ mr: "$3" }}
+  >
+    New
+  </Button>
+);
diff --git a/frontend/pages/team/components/header-actions.tsx b/frontend/pages/team/components/header-actions.tsx
new file mode 100644
index 0000000..0447578
--- /dev/null
+++ b/frontend/pages/team/components/header-actions.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+import MenuButton from "@/components/ui/menu-button";
+import { Button } from "tamagui";
+import Icons from "@/components/ui/icons";
+import { teamFormModal } from "./team-form";
+
+type Props = {
+  team: any;
+};
+
+export default function HeaderActions({ team }: Props) {
+  return (
+    <MenuButton
+      placement="bottom-end"
+      width={200}
+      trigger={
+        <Button
+          circular
+          bg="$colorTransparent"
+          mr="$2"
+          icon={<Icons name="dots-vertical" size={20} />}
+        />
+      }
+    >
+      <MenuButton.Item
+        icon={<Icons name="pencil" size={16} />}
+        onPress={() => teamFormModal.onOpen(team)}
+      >
+        Update Team
+      </MenuButton.Item>
+    </MenuButton>
+  );
+}
diff --git a/frontend/pages/team/components/invite-form.tsx b/frontend/pages/team/components/invite-form.tsx
new file mode 100644
index 0000000..5597084
--- /dev/null
+++ b/frontend/pages/team/components/invite-form.tsx
@@ -0,0 +1,84 @@
+import Icons from "@/components/ui/icons";
+import Modal from "@/components/ui/modal";
+import { useZForm } from "@/hooks/useZForm";
+import { createDisclosure } from "@/lib/utils";
+import React from "react";
+import { ScrollView, XStack } from "tamagui";
+import { InputField } from "@/components/ui/input";
+import FormField from "@/components/ui/form";
+import { useInviteMutation } from "../hooks/query";
+import { ErrorAlert } from "@/components/ui/alert";
+import Button from "@/components/ui/button";
+import {
+  InviteSchema,
+  inviteSchema,
+  teamMemberRoles,
+} from "../schema/team-form";
+import { SelectField } from "@/components/ui/select";
+
+export const inviteFormModal = createDisclosure<InviteSchema>();
+
+const InviteForm = () => {
+  const { data } = inviteFormModal.use();
+  const form = useZForm(inviteSchema, data);
+  const invite = useInviteMutation(data?.teamId || "");
+
+  const onSubmit = form.handleSubmit((values) => {
+    invite.mutate(values, {
+      onSuccess: () => {
+        inviteFormModal.onClose();
+        form.reset();
+      },
+    });
+  });
+
+  return (
+    <Modal
+      disclosure={inviteFormModal}
+      title="Invite"
+      description="Add new team member."
+      maxHeight={360}
+    >
+      <ErrorAlert mx="$4" mb="$4" error={invite.error} />
+
+      <ScrollView contentContainerStyle={{ padding: "$4", pt: 0, gap: "$4" }}>
+        <FormField label="Username/Email">
+          <InputField
+            f={1}
+            form={form}
+            name="username"
+            placeholder="john.doe"
+          />
+        </FormField>
+        <FormField label="Role">
+          <SelectField
+            items={teamMemberRoles}
+            form={form}
+            name="role"
+            placeholder="Select Role..."
+          />
+        </FormField>
+      </ScrollView>
+
+      <XStack p="$4" gap="$4">
+        <Button
+          flex={1}
+          onPress={inviteFormModal.onClose}
+          bg="$colorTransparent"
+        >
+          Cancel
+        </Button>
+        <Button
+          flex={1}
+          icon={<Icons name="account-plus" size={18} />}
+          onPress={onSubmit}
+          isLoading={invite.isPending}
+        >
+          Invite User
+        </Button>
+      </XStack>
+    </Modal>
+  );
+};
+
+export default InviteForm;
diff --git a/frontend/pages/team/components/member-list.tsx b/frontend/pages/team/components/member-list.tsx
new file mode 100644
index 0000000..a0f3890
--- /dev/null
+++ b/frontend/pages/team/components/member-list.tsx
@@ -0,0 +1,89 @@
+import React, { useMemo, useState } from "react";
+import { Avatar, Button, ListItem, View, YGroup } from "tamagui";
+import MenuButton from "@/components/ui/menu-button";
+import Icons from "@/components/ui/icons";
+import SearchInput from "@/components/ui/search-input";
+
+type Props = {
+  members?: any[];
+  allowWrite?: boolean;
+};
+
+const MemberList = ({ members, allowWrite }: Props) => {
+  const [search, setSearch] = useState("");
+
+  const memberList = useMemo(() => {
+    let items = members || [];
+
+    if (search) {
+      items = items.filter((item: any) => {
+        const q = search.toLowerCase();
+        return (
+          item.user?.name.toLowerCase().includes(q) ||
+          item.user?.username.toLowerCase().includes(q) ||
+          item.user?.email.toLowerCase().includes(q)
+        );
+      });
+    }
+
+    return items;
+  }, [members, search]);
+
+  return (
+    <View mt="$4">
+      <SearchInput
+        placeholder="Search member.."
+        value={search}
+        onChangeText={setSearch}
+      />
+
+      <YGroup bordered mt="$4">
+        {memberList?.map((member: any) => (
+          <YGroup.Item key={member.userId}>
+            <ListItem
+              hoverTheme
+              title={member.user?.name}
+              subTitle={member.role}
+              pr="$2"
+              icon={
+                <Avatar size="$3" circular>
+                  <Avatar.Fallback bg="$blue5" />
+                </Avatar>
+              }
+              iconAfter={
+                allowWrite ? <MemberActionButton member={member} /> : undefined
+              }
+            />
+          </YGroup.Item>
+        ))}
+      </YGroup>
+    </View>
+  );
+};
+
+type MemberActionButtonProps = {
+  member: any;
+};
+
+const MemberActionButton = ({ member }: MemberActionButtonProps) => (
+  <MenuButton
+    size="$1"
+    placement="bottom-end"
+    trigger={
+      <Button
+        icon={<Icons name="dots-vertical" size={20} />}
+        circular
+        bg="$colorTransparent"
+      />
+    }
+  >
+    <MenuButton.Item icon={<Icons name="account-key" size={16} />}>
+      Change Role
+    </MenuButton.Item>
+    <MenuButton.Item color="$red10" icon={<Icons name="trash-can" size={16} />}>
+      Remove Member
+    </MenuButton.Item>
+  </MenuButton>
+);
+
+export default MemberList;
diff --git a/frontend/pages/team/components/team-form.tsx b/frontend/pages/team/components/team-form.tsx
new file mode 100644
index 0000000..7b973ab
--- /dev/null
+++ b/frontend/pages/team/components/team-form.tsx
@@ -0,0 +1,73 @@
+import Icons from "@/components/ui/icons";
+import Modal from "@/components/ui/modal";
+import { useZForm } from "@/hooks/useZForm";
+import { createDisclosure } from "@/lib/utils";
+import React from "react";
+import { ScrollView, XStack } from "tamagui";
+import { InputField } from "@/components/ui/input";
+import FormField from "@/components/ui/form";
+import { useSaveTeam } from "../hooks/query";
+import { ErrorAlert } from "@/components/ui/alert";
+import Button from "@/components/ui/button";
+import { TeamFormSchema, teamFormSchema } from "../schema/team-form";
+
+export const teamFormModal = createDisclosure<TeamFormSchema>();
+
+const TeamForm = () => {
+  const { data } = teamFormModal.use();
+  const form = useZForm(teamFormSchema, data);
+  const isEditing = data?.id != null;
+
+  const saveMutation = useSaveTeam();
+
+  const onSubmit = form.handleSubmit((values) => {
+    saveMutation.mutate(values, {
+      onSuccess: () => {
+        teamFormModal.onClose();
+        form.reset();
+      },
+    });
+  });
+
+  return (
+    <Modal
+      disclosure={teamFormModal}
+      title="Team"
+      description={`${isEditing ? "Edit" : "Add new"} team.`}
+      maxHeight={320}
+    >
+      <ErrorAlert mx="$4" mb="$4" error={saveMutation.error} />
+
+      <ScrollView contentContainerStyle={{ padding: "$4", pt: 0, gap: "$4" }}>
+        <FormField label="Name">
+          <InputField
+            f={1}
+            form={form}
+            name="name"
+            placeholder="Team Name..."
+          />
+        </FormField>
+
+        <FormField label="Icon">
+          <InputField form={form} name="icon" placeholder="Icon" />
+        </FormField>
+      </ScrollView>
+
+      <XStack p="$4" gap="$4">
+        <Button flex={1} onPress={teamFormModal.onClose} bg="$colorTransparent">
+          Cancel
+        </Button>
+        <Button
+          flex={1}
+          icon={<Icons name="content-save" size={18} />}
+          onPress={onSubmit}
+          isLoading={saveMutation.isPending}
+        >
+          Save
+        </Button>
+      </XStack>
+    </Modal>
+  );
+};
+
+export default TeamForm;
diff --git a/frontend/pages/team/hooks/query.ts b/frontend/pages/team/hooks/query.ts
new file mode 100644
index 0000000..1fbf0c4
--- /dev/null
+++ b/frontend/pages/team/hooks/query.ts
@@ -0,0 +1,53 @@
+import api from "@/lib/api";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { InviteSchema, TeamFormSchema } from "../schema/team-form";
+import queryClient from "@/lib/queryClient";
+import { setTeam, useTeamId } from "@/stores/auth";
+import { router } from "expo-router";
+
+export const useTeams = () => {
+  return useQuery({
+    queryKey: ["teams"],
+    queryFn: () => api("/teams"),
+    select: (i) => i.rows,
+  });
+};
+
+export const useTeam = () => {
+  const teamId = useTeamId();
+  return useQuery({
+    queryKey: ["teams", teamId],
+    queryFn: () => api(`/teams/${teamId}`),
+  });
+};
+
+export const useSaveTeam = () => {
+  return useMutation({
+    mutationFn: async (body: TeamFormSchema) => {
+      return body.id
+        ? api(`/teams/${body.id}`, { method: "PUT", body })
+        : api(`/teams`, { method: "POST", body });
+    },
+    onError: (e) => console.error(e),
+    onSuccess: (res, body) => {
+      queryClient.invalidateQueries({ queryKey: ["teams"] });
+
+      if (!body.id && res.id) {
+        setTeam(res.id);
+        router.push("/team");
+      }
+    },
+  });
+};
+
+export const useInviteMutation = (teamId: string | null) => {
+  return useMutation({
+    mutationFn: async (body: InviteSchema) => {
+      return api(`/teams/${teamId}/invite`, { method: "POST", body });
+    },
+    onError: (e) => console.error(e),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["teams", teamId] });
+    },
+  });
+};
diff --git a/frontend/pages/team/page.tsx b/frontend/pages/team/page.tsx
new file mode 100644
index 0000000..070c99d
--- /dev/null
+++ b/frontend/pages/team/page.tsx
@@ -0,0 +1,77 @@
+import {
+  View,
+  Text,
+  ScrollView,
+  ListItem,
+  YGroup,
+  Button,
+  Avatar,
+  AvatarFallback,
+  XStack,
+} from "tamagui";
+import React from "react";
+import { useTeam } from "./hooks/query";
+import Drawer from "expo-router/drawer";
+import { useTeamId } from "@/stores/auth";
+import { Redirect } from "expo-router";
+import HeaderActions from "./components/header-actions";
+import Icons from "@/components/ui/icons";
+import tamaguiConfig from "@/tamagui.config";
+import MemberList from "./components/member-list";
+import { useUser } from "@/hooks/useUser";
+import InviteForm, { inviteFormModal } from "./components/invite-form";
+
+export default function TeamPage() {
+  const teamId = useTeamId();
+  const { isPending, data } = useTeam();
+  const user = useUser();
+
+  if (!teamId || (!isPending && !data)) {
+    return <Redirect href="/" />;
+  }
+
+  const canWrite = user?.teamCanWrite(teamId);
+
+  return (
+    <>
+      <Drawer.Screen
+        options={{
+          headerTitle: data ? `${data.icon} ${data.name}` : undefined,
+          headerRight: () => <HeaderActions team={data} />,
+        }}
+      />
+
+      <ScrollView
+        contentContainerStyle={{
+          padding: "$4",
+          maxWidth: tamaguiConfig.media.xs.maxWidth,
+        }}
+      >
+        <XStack alignItems="flex-end">
+          <Text fontSize="$8" f={1} mb="$2">
+            Team Members
+          </Text>
+          {canWrite && (
+            <Button
+              icon={<Icons name="account-plus" size={16} />}
+              onPress={() =>
+                inviteFormModal.onOpen({ teamId, username: "", role: "member" })
+              }
+            >
+              Invite
+            </Button>
+          )}
+        </XStack>
+        <Text>
+          {canWrite
+            ? "Manage or view team members here"
+            : "View your team members here"}
+        </Text>
+
+        <MemberList members={data?.members} allowWrite={canWrite} />
+
+        <InviteForm />
+      </ScrollView>
+    </>
+  );
+}
diff --git a/frontend/pages/team/schema/team-form.ts b/frontend/pages/team/schema/team-form.ts
new file mode 100644
index 0000000..ef362b7
--- /dev/null
+++ b/frontend/pages/team/schema/team-form.ts
@@ -0,0 +1,26 @@
+import { SelectItem } from "@/components/ui/select";
+import { z } from "zod";
+
+export const teamFormSchema = z.object({
+  id: z.string().ulid().nullish(),
+  name: z.string().min(1, { message: "Name is required" }),
+  icon: z.string().emoji("Icon is not valid."),
+});
+
+export type TeamFormSchema = z.infer<typeof teamFormSchema>;
+
+export const inviteSchema = z.object({
+  teamId: z.string().ulid(),
+  username: z.string().min(1, { message: "Username/email is required" }),
+  role: z.enum(["owner", "admin", "member"], {
+    errorMap: () => ({ message: "Role is required" }),
+  }),
+});
+
+export const teamMemberRoles: SelectItem[] = [
+  { label: "Owner", value: "owner" },
+  { label: "Admin", value: "admin" },
+  { label: "Member", value: "member" },
+];
+
+export type InviteSchema = z.infer<typeof inviteSchema>;
diff --git a/server/app/hosts/repository.go b/server/app/hosts/repository.go
index 4e9830b..ce45af9 100644
--- a/server/app/hosts/repository.go
+++ b/server/app/hosts/repository.go
@@ -56,10 +56,6 @@ func (r *Hosts) Exists(id string) (bool, error) {
 	return count > 0, ret.Error
 }
 
-func (r *Hosts) Delete(id string) error {
-	return r.db.Delete(&models.Host{Model: models.Model{ID: id}}).Error
-}
-
 func (r *Hosts) Create(item *models.Host) error {
 	return r.db.Create(item).Error
 }
@@ -67,3 +63,7 @@ func (r *Hosts) Create(item *models.Host) error {
 func (r *Hosts) Update(id string, item *models.Host) error {
 	return r.db.Where("id = ?", id).Updates(item).Error
 }
+
+func (r *Hosts) Delete(id string) error {
+	return r.db.Delete(&models.Host{Model: models.Model{ID: id}}).Error
+}
diff --git a/server/app/router.go b/server/app/router.go
index 94751ec..517de11 100644
--- a/server/app/router.go
+++ b/server/app/router.go
@@ -4,6 +4,7 @@ import (
 	"github.com/gofiber/fiber/v2"
 	"rul.sh/vaulterm/app/hosts"
 	"rul.sh/vaulterm/app/keychains"
+	"rul.sh/vaulterm/app/teams"
 	"rul.sh/vaulterm/app/ws"
 )
 
@@ -12,6 +13,7 @@ func InitRouter(app *fiber.App) {
 	routes := []Router{
 		hosts.Router,
 		keychains.Router,
+		teams.Router,
 		ws.Router,
 	}
 
diff --git a/server/app/teams/repository.go b/server/app/teams/repository.go
index efe8aca..fb84366 100644
--- a/server/app/teams/repository.go
+++ b/server/app/teams/repository.go
@@ -2,6 +2,7 @@ package teams
 
 import (
 	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
 	"rul.sh/vaulterm/db"
 	"rul.sh/vaulterm/models"
 	"rul.sh/vaulterm/utils"
@@ -22,17 +23,50 @@ func NewRepository(r *Teams) *Teams {
 
 func (r *Teams) GetAll() ([]*models.Team, error) {
 	var rows []*models.Team
-	ret := r.db.Order("created_at DESC").Find(&rows)
+	query := r.db.Order("created_at ASC")
+
+	if !r.User.IsAdmin() {
+		query = query.
+			Joins("JOIN team_members ON team_members.team_id = teams.id").
+			Where("team_members.user_id = ?", r.User.ID)
+	}
+
+	ret := query.Find(&rows)
 	return rows, ret.Error
 }
 
 func (r *Teams) Create(data *models.Team) error {
-	return r.db.Create(data).Error
+	return r.db.Transaction(func(tx *gorm.DB) error {
+		if err := tx.Create(data).Error; err != nil {
+			return err
+		}
+
+		if r.User.ID != "" {
+			ret := tx.Create(&models.TeamMembers{
+				UserID: r.User.ID,
+				TeamID: data.ID,
+				Role:   models.TeamRoleOwner,
+			})
+			if ret.Error != nil {
+				return ret.Error
+			}
+		}
+
+		return nil
+	})
 }
 
-func (r *Teams) Get(id string) (*models.Team, error) {
+func (r *Teams) Get(opt GetOptions) (*models.Team, error) {
+	query := r.db.Where("teams.id = ?", opt.ID)
+
+	if opt.WithMembers {
+		query = query.Preload("Members.User", func(db *gorm.DB) *gorm.DB {
+			return db.Select("users.id", "users.name", "users.username", "users.email")
+		})
+	}
+
 	var data models.Team
-	if err := r.db.Where("id = ?", id).First(&data).Error; err != nil {
+	if err := query.First(&data).Error; err != nil {
 		return nil, err
 	}
 
@@ -48,3 +82,26 @@ func (r *Teams) Exists(id string) (bool, error) {
 func (r *Teams) Update(id string, item *models.Team) error {
 	return r.db.Where("id = ?", id).Updates(item).Error
 }
+
+func (r *Teams) Delete(id string) error {
+	return r.db.Transaction(func(tx *gorm.DB) error {
+		if err := tx.Where("team_id = ?", id).Delete(&models.TeamMembers{}).Error; err != nil {
+			return err
+		}
+		if err := tx.Where("id = ?", id).Delete(&models.Team{}).Error; err != nil {
+			return err
+		}
+		return nil
+	})
+}
+
+func (r *Teams) Invite(teamId string, userId string, role string) error {
+	ret := r.db.
+		Clauses(clause.OnConflict{DoNothing: true}).
+		Create(&models.TeamMembers{
+			TeamID: teamId,
+			UserID: userId,
+			Role:   role,
+		})
+	return ret.Error
+}
diff --git a/server/app/teams/router.go b/server/app/teams/router.go
new file mode 100644
index 0000000..980ee38
--- /dev/null
+++ b/server/app/teams/router.go
@@ -0,0 +1,149 @@
+package teams
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/gofiber/fiber/v2"
+	"rul.sh/vaulterm/app/users"
+	"rul.sh/vaulterm/models"
+	"rul.sh/vaulterm/utils"
+)
+
+func Router(app fiber.Router) {
+	router := app.Group("/teams")
+
+	router.Get("/", getAll)
+	router.Get("/:id", getById)
+	router.Post("/", create)
+	router.Put("/:id", update)
+	router.Delete("/:id", delete)
+	router.Post("/:id/invite", invite)
+}
+
+func getAll(c *fiber.Ctx) error {
+	user := utils.GetUser(c)
+	repo := NewRepository(&Teams{User: user})
+
+	rows, err := repo.GetAll()
+	if err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	return c.JSON(fiber.Map{"rows": rows})
+}
+
+func getById(c *fiber.Ctx) error {
+	user := utils.GetUser(c)
+	repo := NewRepository(&Teams{User: user})
+
+	id := c.Params("id")
+	data, _ := repo.Get(GetOptions{ID: id, WithMembers: true})
+	if data == nil || !user.IsInTeam(&id) {
+		return utils.ResponseError(c, errors.New("team not found"), 404)
+	}
+
+	return c.JSON(data)
+}
+
+func create(c *fiber.Ctx) error {
+	var body CreateTeamSchema
+	if err := c.BodyParser(&body); err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	user := utils.GetUser(c)
+	repo := NewRepository(&Teams{User: user})
+
+	item := &models.Team{
+		Name: body.Name,
+		Icon: body.Icon,
+	}
+
+	if err := repo.Create(item); err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	return c.Status(http.StatusCreated).JSON(item)
+}
+
+func update(c *fiber.Ctx) error {
+	var body CreateTeamSchema
+	if err := c.BodyParser(&body); err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	user := utils.GetUser(c)
+	repo := NewRepository(&Teams{User: user})
+
+	id := c.Params("id")
+	data, _ := repo.Get(GetOptions{ID: id})
+	if data == nil {
+		return utils.ResponseError(c, errors.New("team not found"), 404)
+	}
+	if !user.TeamCanWrite(&id) {
+		return utils.ResponseError(c, errors.New("no access"), 403)
+	}
+
+	item := &models.Team{
+		Name: body.Name,
+		Icon: body.Icon,
+	}
+
+	if err := repo.Update(id, item); err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	return c.JSON(item)
+}
+
+func delete(c *fiber.Ctx) error {
+	user := utils.GetUser(c)
+	repo := NewRepository(&Teams{User: user})
+
+	id := c.Params("id")
+	data, _ := repo.Get(GetOptions{ID: id})
+	if data == nil {
+		return utils.ResponseError(c, errors.New("team not found"), 404)
+	}
+	if !user.TeamCanWrite(&id) {
+		return utils.ResponseError(c, errors.New("no access"), 403)
+	}
+
+	if err := repo.Delete(id); err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	return c.JSON(true)
+}
+
+func invite(c *fiber.Ctx) error {
+	var body InviteTeamSchema
+	if err := c.BodyParser(&body); err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	user := utils.GetUser(c)
+	repo := NewRepository(&Teams{User: user})
+
+	id := c.Params("id")
+	exist, _ := repo.Exists(id)
+	if !exist {
+		return utils.ResponseError(c, errors.New("team not found"), 404)
+	}
+	if !user.TeamCanWrite(&id) {
+		return utils.ResponseError(c, errors.New("no access"), 403)
+	}
+
+	userRepo := users.NewRepository(&users.Users{User: user})
+	userData, _ := userRepo.Find(body.Username)
+	if userData.ID == "" {
+		return utils.ResponseError(c, errors.New("user not found"), 404)
+	}
+
+	if err := repo.Invite(id, userData.ID, body.Role); err != nil {
+		return utils.ResponseError(c, err, 500)
+	}
+
+	return c.JSON(true)
+}
diff --git a/server/app/teams/schema.go b/server/app/teams/schema.go
new file mode 100644
index 0000000..9dd3af9
--- /dev/null
+++ b/server/app/teams/schema.go
@@ -0,0 +1,16 @@
+package teams
+
+type CreateTeamSchema struct {
+	Name string `json:"name"`
+	Icon string `json:"icon"`
+}
+
+type GetOptions struct {
+	ID          string
+	WithMembers bool
+}
+
+type InviteTeamSchema struct {
+	Username string `json:"username"`
+	Role     string `json:"role"`
+}
diff --git a/server/app/users/repository.go b/server/app/users/repository.go
new file mode 100644
index 0000000..e3ebb1d
--- /dev/null
+++ b/server/app/users/repository.go
@@ -0,0 +1,28 @@
+package users
+
+import (
+	"gorm.io/gorm"
+	"rul.sh/vaulterm/db"
+	"rul.sh/vaulterm/models"
+	"rul.sh/vaulterm/utils"
+)
+
+type Users struct {
+	db   *gorm.DB
+	User *utils.UserContext
+}
+
+func NewRepository(r *Users) *Users {
+	if r == nil {
+		r = &Users{}
+	}
+	r.db = db.Get()
+	return r
+}
+
+func (r *Users) Find(username string) (*models.User, error) {
+	var user models.User
+	ret := r.db.Where("username = ? OR email = ?", username, username).First(&user)
+
+	return &user, ret.Error
+}
diff --git a/server/models/team.go b/server/models/team.go
index 86e242b..776f631 100644
--- a/server/models/team.go
+++ b/server/models/team.go
@@ -21,7 +21,7 @@ type Team struct {
 
 type TeamMembers struct {
 	TeamID    string    `json:"teamId" gorm:"primarykey;type:varchar(26)"`
-	Team      Team      `json:"team"`
+	Team      Team      `json:"-"`
 	UserID    string    `json:"userId" gorm:"primarykey;type:varchar(26)"`
 	User      User      `json:"user"`
 	Role      string    `json:"role" gorm:"type:varchar(16)"`