diff --git a/README.md b/README.md index 8218610d..5bf1b677 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Add the following to your `.env` file: ```dotenv NEXT_PUBLIC_SUPABASE_ANON_KEY= NEXT_PUBLIC_SUPABASE_URL= +SUPABASE_SERVICE_KEY= ``` Generate types and start the dev server: @@ -43,7 +44,8 @@ Vercel common secrets: Vercel environment secrets: - LEMON_SQUEEZY_API_KEY -- LEMON_SQUEEZY_VARIANT_ID +- LEMON_SQUEEZY_VARIANT_ID_PRO +- LEMON_SQUEEZY_VARIANT_ID_TEAM - LEMON_SQUEEZY_WEBHOOK_SECRET - RESEND_API_KEY - SUPABASE_SERVICE_KEY diff --git a/app/(pages)/(with-nav)/layout.tsx b/app/(pages)/(with-nav)/layout.tsx index ef344971..d2221e62 100644 --- a/app/(pages)/(with-nav)/layout.tsx +++ b/app/(pages)/(with-nav)/layout.tsx @@ -7,6 +7,7 @@ import NotificationsSubscription from '@/_components/notifications-subscription' import canInsertSubjectOnCurrentPlan from '@/_queries/can-insert-subject-on-current-plan'; import countNotifications from '@/_queries/count-notifications'; import getCurrentUser from '@/_queries/get-current-user'; +import listTeams from '@/_queries/list-teams'; import Bars3Icon from '@heroicons/react/24/outline/Bars3Icon'; import DocumentTextIcon from '@heroicons/react/24/outline/DocumentTextIcon'; import HomeIcon from '@heroicons/react/24/outline/HomeIcon'; @@ -20,14 +21,20 @@ interface LayoutProps { } const Layout = async ({ children }: LayoutProps) => { - const [{ data: canCreateSubject }, { count }, user] = await Promise.all([ - canInsertSubjectOnCurrentPlan(), - countNotifications(), - getCurrentUser(), - ]); - + const user = await getCurrentUser(); if (!user) return null; + const [{ data: canCreateSubject }, { count }, { data: teams }] = + await Promise.all([ + user.app_metadata.is_client + ? Promise.resolve({ data: false }) + : canInsertSubjectOnCurrentPlan(), + countNotifications(), + user.app_metadata.is_client ? Promise.resolve({ data: [] }) : listTeams(), + ]); + + if (!teams) return null; + return ( <>
@@ -61,7 +68,7 @@ const Layout = async ({ children }: LayoutProps) => {
Inbox - {!user.user_metadata.is_client && ( + {!user.app_metadata.is_client && ( <> @@ -77,7 +84,10 @@ const Layout = async ({ children }: LayoutProps) => { Add new menu - + @@ -109,7 +119,7 @@ const Layout = async ({ children }: LayoutProps) => { )} - + diff --git a/app/(pages)/(with-nav)/subjects/page.tsx b/app/(pages)/(with-nav)/subjects/page.tsx index ebb46047..bf1046f5 100644 --- a/app/(pages)/(with-nav)/subjects/page.tsx +++ b/app/(pages)/(with-nav)/subjects/page.tsx @@ -23,7 +23,7 @@ const Page = async () => { teamSubjects, } = subjects.reduce( (acc, subject) => { - if (subject.team_id === user.id) { + if (subject.team_id === user.app_metadata.active_team_id) { if (subject.archived) acc.archivedTeamSubjects.push(subject); else acc.teamSubjects.push(subject); } else { @@ -48,17 +48,17 @@ const Page = async () => { {!clientSubjects.length && !teamSubjects.length && ( - {user.user_metadata.is_client ? ( + {user.app_metadata.is_client ? ( 'No active subjects.' ) : (
- Create a subject using the - yellow + Create a new subject using + the yellow
- button for eternal glory. + button. Let’s make changes.
)}
diff --git a/app/(pages)/@modal/(layout)/hey/page.tsx b/app/(pages)/@modal/(layout)/hey/page.tsx index 7bdddbd3..fd9da88b 100644 --- a/app/(pages)/@modal/(layout)/hey/page.tsx +++ b/app/(pages)/@modal/(layout)/hey/page.tsx @@ -14,24 +14,22 @@ const Page = async () => { title={`Hey, ${user.user_metadata.first_name}!`} />
-
+

At llog, our mission is to empower behavior professionals and their - clients by crafting intuitive and effective software tools that make - lasting behavior change more attainable. -

-

- Your feedback is invaluable as we evolve and improve. Whether you - have thoughts, ideas, concerns or even dreams, we’d love to - hear from you. Feel free to drop us a message at{' '} - {' '} - or{' '} - - . + clients with tools that make lasting behavior change + more attainable. Your feedback is invaluable as we evolve and + improve. Whether you have thoughts, ideas, concerns or even dreams:{' '} + + {' '} + or{' '} + + . +

Happy behavior hacking!

~ Cade, Founder

diff --git a/app/(pages)/@modal/(layout)/subjects/[subjectId]/edit/page.tsx b/app/(pages)/@modal/(layout)/subjects/[subjectId]/edit/page.tsx index 51ed3da9..5217b3d5 100644 --- a/app/(pages)/@modal/(layout)/subjects/[subjectId]/edit/page.tsx +++ b/app/(pages)/@modal/(layout)/subjects/[subjectId]/edit/page.tsx @@ -2,6 +2,7 @@ import * as Modal from '@/_components/modal'; import PageModalHeader from '@/_components/page-modal-header'; import SubjectForm from '@/_components/subject-form'; import getSubject from '@/_queries/get-subject'; +import listTags from '@/_queries/list-tags'; interface PageProps { params: Promise<{ subjectId: string }>; @@ -9,13 +10,18 @@ interface PageProps { const Page = async ({ params }: PageProps) => { const { subjectId } = await params; - const { data: subject } = await getSubject(subjectId); - if (!subject) return null; + + const [{ data: subject }, { data: tags }] = await Promise.all([ + getSubject(subjectId), + listTags(), + ]); + + if (!subject || !tags) return null; return ( - + ); }; diff --git a/app/(pages)/@modal/(layout)/subjects/[subjectId]/event-types/[eventTypeId]/page.tsx b/app/(pages)/@modal/(layout)/subjects/[subjectId]/event-types/[eventTypeId]/page.tsx index 15b7a065..bec762f8 100644 --- a/app/(pages)/@modal/(layout)/subjects/[subjectId]/event-types/[eventTypeId]/page.tsx +++ b/app/(pages)/@modal/(layout)/subjects/[subjectId]/event-types/[eventTypeId]/page.tsx @@ -20,7 +20,9 @@ const Page = async ({ params }: PageProps) => { ]); if (!subject || !eventType) return null; - const isTeamMember = !!user && subject.team_id === user.id; + + const isTeamMember = + !!user && subject.team_id === user.app_metadata.active_team_id; return ( diff --git a/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/[sessionId]/edit/page.tsx b/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/[sessionId]/edit/page.tsx index 2158a134..cd8dc27d 100644 --- a/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/[sessionId]/edit/page.tsx +++ b/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/[sessionId]/edit/page.tsx @@ -48,8 +48,7 @@ const Page = async ({ params }: PageProps) => { !subject || !subjects || !protocol || - !user || - subject.team_id !== user.id + !user ) { return null; } diff --git a/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/from-session/[sessionId]/page.tsx b/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/from-session/[sessionId]/page.tsx index 7eded957..8a7a3376 100644 --- a/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/from-session/[sessionId]/page.tsx +++ b/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/from-session/[sessionId]/page.tsx @@ -48,8 +48,7 @@ const Page = async ({ params }: PageProps) => { !subject || !subjects || !protocol || - !user || - subject.team_id !== user.id + !user ) { return null; } diff --git a/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/page.tsx b/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/page.tsx index cc7b47a2..102eb49a 100644 --- a/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/page.tsx +++ b/app/(pages)/@modal/(layout)/subjects/[subjectId]/protocols/[protocolId]/sessions/create/[order]/page.tsx @@ -39,8 +39,7 @@ const Page = async ({ params }: PageProps) => { !subject || !subjects || !protocol || - !user || - subject.team_id !== user.id + !user ) { return null; } diff --git a/app/(pages)/@modal/(layout)/subjects/create/page.tsx b/app/(pages)/@modal/(layout)/subjects/create/page.tsx index 2e4f75ab..30d08871 100644 --- a/app/(pages)/@modal/(layout)/subjects/create/page.tsx +++ b/app/(pages)/@modal/(layout)/subjects/create/page.tsx @@ -1,12 +1,18 @@ import * as Modal from '@/_components/modal'; import PageModalHeader from '@/_components/page-modal-header'; import SubjectForm from '@/_components/subject-form'; +import listTags from '@/_queries/list-tags'; -const Page = () => ( - - - - -); +const Page = async () => { + const { data: tags } = await listTags(); + if (!tags) return null; + + return ( + + + + + ); +}; export default Page; diff --git a/app/(pages)/@modal/(layout)/upgrade/loading.tsx b/app/(pages)/@modal/(layout)/teams/[teamId]/edit/loading.tsx similarity index 100% rename from app/(pages)/@modal/(layout)/upgrade/loading.tsx rename to app/(pages)/@modal/(layout)/teams/[teamId]/edit/loading.tsx diff --git a/app/(pages)/@modal/(layout)/teams/[teamId]/edit/page.tsx b/app/(pages)/@modal/(layout)/teams/[teamId]/edit/page.tsx new file mode 100644 index 00000000..1411c89e --- /dev/null +++ b/app/(pages)/@modal/(layout)/teams/[teamId]/edit/page.tsx @@ -0,0 +1,23 @@ +import * as Modal from '@/_components/modal'; +import PageModalHeader from '@/_components/page-modal-header'; +import TeamForm from '@/_components/team-form'; +import getTeam from '@/_queries/get-team'; + +interface PageProps { + params: Promise<{ teamId: string }>; +} + +const Page = async ({ params }: PageProps) => { + const { teamId } = await params; + const { data: team } = await getTeam(teamId); + if (!team) return null; + + return ( + + + + + ); +}; + +export default Page; diff --git a/app/(pages)/@modal/(layout)/teams/[teamId]/upgrade/loading.tsx b/app/(pages)/@modal/(layout)/teams/[teamId]/upgrade/loading.tsx new file mode 100644 index 00000000..e3d948d8 --- /dev/null +++ b/app/(pages)/@modal/(layout)/teams/[teamId]/upgrade/loading.tsx @@ -0,0 +1,5 @@ +import PageModalLoading from '@/_components/page-modal-loading'; + +const Loading = PageModalLoading; + +export default Loading; diff --git a/app/(pages)/@modal/(layout)/upgrade/page.tsx b/app/(pages)/@modal/(layout)/teams/[teamId]/upgrade/page.tsx similarity index 75% rename from app/(pages)/@modal/(layout)/upgrade/page.tsx rename to app/(pages)/@modal/(layout)/teams/[teamId]/upgrade/page.tsx index 331c123a..f88133a2 100644 --- a/app/(pages)/@modal/(layout)/upgrade/page.tsx +++ b/app/(pages)/@modal/(layout)/teams/[teamId]/upgrade/page.tsx @@ -1,12 +1,24 @@ import Button from '@/_components/button'; +import CheckoutButton from '@/_components/checkout-button'; import * as Modal from '@/_components/modal'; import PageModalHeader from '@/_components/page-modal-header'; -import UpgradePlanButton from '@/_components/upgrade-plan-button'; +import SubscriptionVariantName from '@/_constants/enum-subscription-variant-name'; import getCurrentUser from '@/_queries/get-current-user'; +import getTeam from '@/_queries/get-team'; -const Page = async () => { - const user = await getCurrentUser(); - if (!user) return; +interface PageProps { + params: Promise<{ teamId: string }>; +} + +const Page = async ({ params }: PageProps) => { + const { teamId } = await params; + + const [{ data: team }, user] = await Promise.all([ + getTeam(teamId), + getCurrentUser(), + ]); + + if (!team || !user) return; return ( @@ -51,7 +63,11 @@ const Page = async () => { Close - +
diff --git a/app/(pages)/@modal/(layout)/teams/create/loading.tsx b/app/(pages)/@modal/(layout)/teams/create/loading.tsx new file mode 100644 index 00000000..e3d948d8 --- /dev/null +++ b/app/(pages)/@modal/(layout)/teams/create/loading.tsx @@ -0,0 +1,5 @@ +import PageModalLoading from '@/_components/page-modal-loading'; + +const Loading = PageModalLoading; + +export default Loading; diff --git a/app/(pages)/@modal/(layout)/teams/create/page.tsx b/app/(pages)/@modal/(layout)/teams/create/page.tsx new file mode 100644 index 00000000..82250c85 --- /dev/null +++ b/app/(pages)/@modal/(layout)/teams/create/page.tsx @@ -0,0 +1,12 @@ +import * as Modal from '@/_components/modal'; +import PageModalHeader from '@/_components/page-modal-header'; +import TeamForm from '@/_components/team-form'; + +const Page = async () => ( + + + + +); + +export default Page; diff --git a/app/(pages)/subjects/[subjectId]/join/[shareCode]/page.tsx b/app/(pages)/subjects/[subjectId]/join/[shareCode]/page.tsx index d671f515..676d26bb 100644 --- a/app/(pages)/subjects/[subjectId]/join/[shareCode]/page.tsx +++ b/app/(pages)/subjects/[subjectId]/join/[shareCode]/page.tsx @@ -13,9 +13,7 @@ const Page = async ({ params }: PageProps) => { if (!subject) { await ( await createServerSupabaseClient() - ).rpc('join_subject_as_manager', { - share_code: shareCode, - }); + ).rpc('join_subject_as_client', { share_code: shareCode }); } redirect(`/subjects/${subjectId}`); diff --git a/app/_components/account-menu.tsx b/app/_components/account-menu.tsx index 972c6904..3264b279 100644 --- a/app/_components/account-menu.tsx +++ b/app/_components/account-menu.tsx @@ -3,31 +3,44 @@ import Avatar from '@/_components/avatar'; import Button from '@/_components/button'; import * as Drawer from '@/_components/drawer'; -import SubscriptionStatus from '@/_constants/enum-subscription-status'; +import SubscriptionVariantName from '@/_constants/enum-subscription-variant-name'; +import setActiveTeam from '@/_mutations/set-active-team'; import signOut from '@/_mutations/sign-out'; -import getCustomerBillingPortal from '@/_queries/get-customer-billing-portal'; +import getSubscriptionBillingPortal from '@/_queries/get-subscription-billing-portal'; +import { ListTeamsData } from '@/_queries/list-teams'; +import firstIfArray from '@/_utilities/first-if-array'; import ArrowLeftStartOnRectangleIcon from '@heroicons/react/24/outline/ArrowLeftStartOnRectangleIcon'; import AtSymbolIcon from '@heroicons/react/24/outline/AtSymbolIcon'; +import CheckIcon from '@heroicons/react/24/outline/CheckIcon'; import CreditCardIcon from '@heroicons/react/24/outline/CreditCardIcon'; import HeartIcon from '@heroicons/react/24/outline/HeartIcon'; import LockClosedIcon from '@heroicons/react/24/outline/LockClosedIcon'; +import PencilIcon from '@heroicons/react/24/outline/PencilIcon'; +import PlusIcon from '@heroicons/react/24/outline/PlusIcon'; import RocketLaunchIcon from '@heroicons/react/24/outline/RocketLaunchIcon'; -import UserCircleIcon from '@heroicons/react/24/outline/UserCircleIcon'; import { User } from '@supabase/supabase-js'; import { useState, useTransition } from 'react'; +import { twMerge } from 'tailwind-merge'; interface AccountMenuProps { - user: User; + user: NonNullable; + teams: NonNullable; } -const AccountMenu = ({ user }: AccountMenuProps) => { +const AccountMenu = ({ user, teams }: AccountMenuProps) => { const [isSignOutTransitioning, startSignOutTransition] = useTransition(); + const [isChangeTeamsTransitioning, startChangeTeamsTransition] = + useTransition(); + + const [changeTeamsId, setChangeTeamsId] = useState(null); + const [isBillingRedirectLoading, setIsBillingRedirectLoading] = useState(false); - const isSubscribed = - user.app_metadata.subscription_status === SubscriptionStatus.Active; + const activeTeam = teams.find( + (team) => team.id === user.app_metadata.active_team_id, + ); return ( @@ -39,8 +52,12 @@ const AccountMenu = ({ user }: AccountMenuProps) => {
Account @@ -51,53 +68,134 @@ const AccountMenu = ({ user }: AccountMenuProps) => { Account menu - - - Feedback - - + {!user.app_metadata.is_client && ( + <> + {teams.map((team) => { + const isLoading = + isChangeTeamsTransitioning && changeTeamsId === team.id; + + const subscription = firstIfArray(team.subscriptions); + + return ( + + { + e.preventDefault(); + setChangeTeamsId(team.id); + startChangeTeamsTransition(() => + setActiveTeam(team.id), + ); + }} + > + {!isLoading && ( + + )} + {team.name} +
+ {subscription?.variant ?? 'free'} +
+ {((team.id === activeTeam?.id && + !isChangeTeamsTransitioning) || + isLoading) && ( + + )} +
+ + + + + + + + Organization menu + + + + Edit + + { + if (!subscription) return; + e.preventDefault(); + setIsBillingRedirectLoading(true); + + const { url } = + await getSubscriptionBillingPortal( + subscription.id, + ); + + if (url) location.href = url; + else setIsBillingRedirectLoading(false); + }} + > + {subscription ? ( + <> + + Manage billing + + ) : ( + <> + + Upgrade plan + + )} + + + + +
+ ); + })} + + + New organization + + + + )} - + Edit profile + + + Change password + Change email - - - Change password + + + + Give feedback - {!user?.user_metadata?.is_client && ( - <> - - { - if (!isSubscribed) return; - e.preventDefault(); - setIsBillingRedirectLoading(true); - const { url } = await getCustomerBillingPortal(); - if (url) location.href = url; - else setIsBillingRedirectLoading(false); - }} - > - {isSubscribed ? ( - <> - - Manage billing - - ) : ( - <> - - Upgrade plan - - )} - - - )} { }); const router = useRouter(); + const avatar = form.watch('avatar'); return (
startTransition(async () => { - const supabase = createBrowserSupabaseClient(); - - if (!values.avatar) { - await Promise.all([ - supabase.storage.from('profiles').remove([`${user.id}/avatar`]), - supabase.auth.updateUser({ data: { image_uri: null } }), - ]); - } - - if (values.avatar instanceof File) { - await supabase.storage - .from('profiles') - .upload(`${user.id}/avatar`, values.avatar, { upsert: true }); - } + await upsertAvatar({ + avatar: values.avatar, + bucket: 'profiles', + id: user.id, + }); const res = await updateUser({ first_name: values.firstName, @@ -81,13 +73,15 @@ const AccountProfileForm = ({ user }: AccountProfileFormProps) => {
- Profile image - form.setValue('avatar', null)}> - Remove image - + Image + {avatar && ( + form.setValue('avatar', null)}> + Remove image + + )} form.setValue('avatar', files[0])} /> diff --git a/app/_components/add-new-menu-items.tsx b/app/_components/add-new-menu-items.tsx index 6db6403d..2c328001 100644 --- a/app/_components/add-new-menu-items.tsx +++ b/app/_components/add-new-menu-items.tsx @@ -3,13 +3,15 @@ import * as Drawer from '@/_components/drawer'; import Tip from '@/_components/tip'; import PlusIcon from '@heroicons/react/24/outline/PlusIcon'; +import { User } from '@supabase/supabase-js'; import { usePathname } from 'next/navigation'; interface AddNewMenuItemsProps { canCreateSubject: boolean; + user: User; } -const AddNewMenuItems = ({ canCreateSubject }: AddNewMenuItemsProps) => { +const AddNewMenuItems = ({ canCreateSubject, user }: AddNewMenuItemsProps) => { const pathname = usePathname(); const [, type, id] = pathname.split('/'); @@ -44,7 +46,13 @@ const AddNewMenuItems = ({ canCreateSubject }: AddNewMenuItemsProps) => { )} - + New subject diff --git a/app/_components/avatar-dropzone.tsx b/app/_components/avatar-dropzone.tsx index ecea1994..89283fc2 100644 --- a/app/_components/avatar-dropzone.tsx +++ b/app/_components/avatar-dropzone.tsx @@ -25,7 +25,7 @@ const AvatarDropzone = ({ return (
diff --git a/app/_components/checkbox.tsx b/app/_components/checkbox.tsx index 24cd38f0..1c8a6a87 100644 --- a/app/_components/checkbox.tsx +++ b/app/_components/checkbox.tsx @@ -11,7 +11,7 @@ const Checkbox = forwardRef( ({ className, label, name, ...rest }, ref) => (
@@ -23,6 +23,9 @@ const Checkbox = forwardRef( type="checkbox" {...rest} /> +
+ +
( > {label && }
-
), ); diff --git a/app/_components/upgrade-plan-button.tsx b/app/_components/checkout-button.tsx similarity index 58% rename from app/_components/upgrade-plan-button.tsx rename to app/_components/checkout-button.tsx index 7e970b3f..44390a38 100644 --- a/app/_components/upgrade-plan-button.tsx +++ b/app/_components/checkout-button.tsx @@ -1,31 +1,35 @@ 'use client'; import Button from '@/_components/button'; -import SubscriptionStatus from '@/_constants/enum-subscription-status'; +import SubscriptionVariantName from '@/_constants/enum-subscription-variant-name'; import createCustomerCheckout from '@/_mutations/create-customer-checkout'; -import { User } from '@supabase/supabase-js'; import { useState } from 'react'; -interface UpgradePlanButtonProps { - user: User; +interface CheckoutButtonProps { + disabled?: boolean; + teamId: string; + variant: SubscriptionVariantName; } -const UpgradePlanButton = ({ user }: UpgradePlanButtonProps) => { +const CheckoutButton = ({ disabled, teamId, variant }: CheckoutButtonProps) => { const [isBillingRedirectLoading, setIsBillingRedirectLoading] = useState(false); return ( diff --git a/app/_components/event-page.tsx b/app/_components/event-page.tsx index 9248ce00..f2cb0ceb 100644 --- a/app/_components/event-page.tsx +++ b/app/_components/event-page.tsx @@ -25,7 +25,10 @@ const EventPage = async ({ eventId, isPublic, subjectId }: EventPageProps) => { ]); if (!subject || !event || !event.type) return null; - const isTeamMember = !!user && subject.team_id === user.id; + + const isTeamMember = + !!user && subject.team_id === user.app_metadata.active_team_id; + const shareOrSubjects = isPublic ? 'share' : 'subjects'; return ( diff --git a/app/_components/event-select.tsx b/app/_components/event-select.tsx index 94254226..5d5a072c 100644 --- a/app/_components/event-select.tsx +++ b/app/_components/event-select.tsx @@ -3,18 +3,15 @@ import { EventFormValues } from '@/_components/event-form'; import InputRoot from '@/_components/input-root'; import * as Label from '@/_components/label'; -import Select, { IOption } from '@/_components/select'; +import Select from '@/_components/select-v2'; import createInputOption from '@/_mutations/create-input-option'; import { Database } from '@/_types/database'; import { InputSettingsJson } from '@/_types/input-settings-json'; import { useRouter } from 'next/navigation'; -import { useTransition } from 'react'; import { ControllerRenderProps } from 'react-hook-form'; -import { PropsValue } from 'react-select'; interface EventSelectProps { field: ControllerRenderProps; - id: string; input: Pick< Database['public']['Tables']['inputs']['Row'], 'id' | 'label' | 'settings' | 'type' @@ -25,63 +22,41 @@ interface EventSelectProps { }; } -const EventSelect = ({ field, id, input }: EventSelectProps) => { - const [isTransitioning, startTransition] = useTransition(); - - const optionOrOptions = - input.type === 'multi_select' ? 'options' : 'an option'; - +const EventSelect = ({ field, input }: EventSelectProps) => { const inputSettings = input.settings as InputSettingsJson; - - const placeholder = inputSettings?.isCreatable - ? `Select ${optionOrOptions} or create your own…` - : `Select ${optionOrOptions}…`; - const router = useRouter(); return ( - - {input.label} - + {input.label} { + if (e.key === 'Enter') { + e.preventDefault(); + onCreate(); + } + }} + placeholder={`Add ${optionName}…`} + ref={ref} + {...rest} + /> +
+ } + label={`Add ${optionName}`} + loading={isTransitioning} + loadingText={`Adding ${optionName}…`} + onClick={onCreate} + /> +
+
+ ); +}); + +SelectCreateOptionInput.displayName = 'CreateInput'; + +export interface Option { + id: string; + label: React.ReactNode; +} + +interface SelectProps { + isCreatable?: boolean; + isMulti?: boolean; + isReorderable?: boolean; + onChange: (value: string | string[]) => void; + onCreateOption: (value: string) => Promise; + optionName?: string; + options: Option[]; + placeholder?: string; + value: string | string[]; +} + +const Select = ({ + isCreatable, + isMulti, + isReorderable, + onChange, + onCreateOption, + optionName = 'option', + options, + placeholder, + value, +}: SelectProps) => { + const [, startTransition] = React.useTransition(); + const [filteredOptions, setFilteredOptions] = React.useState([]); + const filterRef = React.useRef(null); + const optionsLen = options.length; + const optionsMap = new Map(options.map((option) => [option.id, option])); + const prevOptionsLen = usePrevious(optionsLen); + + const toggleGroupProps = isMulti + ? { + onValueChange: onChange as (value: string[]) => void, + type: 'multiple' as const, + value: value as string[], + } + : { + onValueChange: onChange as (value: string) => void, + type: 'single' as const, + value: value as string, + }; + + const fuse = React.useMemo( + () => new Fuse(options, { keys: ['label'], threshold: 0.3 }), + [options], + ); + + const filterOptions = React.useCallback( + (value: string) => + setFilteredOptions(fuse.search(value).map((result) => result.item)), + [fuse], + ); + + const onFilter = ({ + target: { value }, + }: React.ChangeEvent) => + startTransition(() => { + if (value) filterOptions(value); + else setFilteredOptions(options); + }); + + React.useEffect(() => { + if (optionsLen !== prevOptionsLen) { + setFilteredOptions(options); + if (filterRef.current?.value) filterOptions(filterRef.current.value); + } + }, [filterOptions, options, optionsLen, prevOptionsLen]); + + if (options.length < 8 && !isReorderable) { + return ( + <> + {!!options.length && ( + + {options.map((option) => ( + + {option.label} + + ))} + + )} + {isCreatable && ( + + )} + + ); + } + + return ( + + {isMulti && !!value.length && ( + + {options + .filter((option) => value.includes(option.id)) + .map((option) => ( + + {option.label} + + ))} + + )} +
+ + {!isMulti && value ? ( + optionsMap.get(value as string)?.label + ) : ( + + {placeholder ?? + (isMulti + ? `Select ${optionName}s…` + : `Select an ${optionName}…`)} + + )} + + +
+ + + + Select ${optionName}s + + + {filteredOptions.map((option) => ( + + {option.label} + + ))} + + + + + + + +
+ ); +}; + +Select.displayName = 'Select'; + +export default Select; diff --git a/app/_components/session-page.tsx b/app/_components/session-page.tsx index 905a18fe..8ede8ed7 100644 --- a/app/_components/session-page.tsx +++ b/app/_components/session-page.tsx @@ -50,7 +50,8 @@ const SessionPage = async ({ const currentSession = protocol.sessions.find((s) => s.id === sessionId); if (!currentSession) return null; - const isTeamMember = !!user && subject.team_id === user.id; + const isTeamMember = + !!user && subject.team_id === user.app_metadata.active_team_id; const shareOrSubjects = isPublic ? 'share' : 'subjects'; let { diff --git a/app/_components/session-use-template-drawer.tsx b/app/_components/session-use-template-drawer.tsx index e90b367b..28e457b2 100644 --- a/app/_components/session-use-template-drawer.tsx +++ b/app/_components/session-use-template-drawer.tsx @@ -2,7 +2,7 @@ import Button from '@/_components/button'; import * as Drawer from '@/_components/drawer'; -import Select, { IOption } from '@/_components/select'; +import Select, { IOption } from '@/_components/select-v1'; import getTemplateData from '@/_queries/get-template-data'; import { ListInputsBySubjectIdData } from '@/_queries/list-inputs-by-subject-id'; import { ListTemplatesData } from '@/_queries/list-templates'; diff --git a/app/_components/sessions-page.tsx b/app/_components/sessions-page.tsx index 2e8da7e1..d8272d76 100644 --- a/app/_components/sessions-page.tsx +++ b/app/_components/sessions-page.tsx @@ -34,7 +34,8 @@ const SessionsPage = async ({ ]); if (!subject) return null; - const isTeamMember = !!user && subject.team_id === user.id; + const isTeamMember = + !!user && subject.team_id === user.app_metadata.active_team_id; const { data: protocol } = isPublic ? await getPublicProtocolWithSessionsAndEvents(protocolId) @@ -85,8 +86,8 @@ const SessionsPage = async ({ {!!sessionsReversed.length && (
    {sessionsReversed.map((session) => { - const completedModules = session.modules.filter( - (m) => m.event.length, + const completedModules = session.modules.filter((m) => + firstIfArray(m.event), ); const firstCompletedEvent = firstIfArray( diff --git a/app/_components/sign-up-form.tsx b/app/_components/sign-up-form.tsx index 73778b25..c379a39e 100644 --- a/app/_components/sign-up-form.tsx +++ b/app/_components/sign-up-form.tsx @@ -13,10 +13,18 @@ interface SignUpFormProps { const SignUpForm = ({ next }: SignUpFormProps) => { const [state, action] = useActionState(signUp.bind(null, { next }), { - defaultValues: { email: '', firstName: '', lastName: '', password: '' }, + defaultValues: { + email: '', + firstName: '', + lastName: '', + organization: '', + password: '', + }, error: '', }); + const isClient = !!next?.includes('/join/'); + return (
    @@ -37,6 +45,17 @@ const SignUpForm = ({ next }: SignUpFormProps) => { />
    + {!isClient && ( + + + Organization (optional) + + + + )} Email address ; + tags: NonNullable; } export interface SubjectFormValues { avatar: File | string | null; data: SubjectDataJson; name: string; + tags: string[]; } -const SubjectForm = ({ subject }: SubjectFormProps) => { +const SubjectForm = ({ subject, tags }: SubjectFormProps) => { const [isTransitioning, startTransition] = useTransition(); const router = useRouter(); const subjectData = subject?.data as SubjectDataJson; @@ -39,6 +44,7 @@ const SubjectForm = ({ subject }: SubjectFormProps) => { avatar: subject?.image_uri, data: subjectData, name: subject?.name, + tags: subject?.tags?.map((t) => t.tag_id) ?? [], }, }); @@ -47,14 +53,24 @@ const SubjectForm = ({ subject }: SubjectFormProps) => { name: 'data.links', }); + const avatar = form.watch('avatar'); + return ( startTransition(async () => { + if (subject) { + await upsertAvatar({ + avatar: values.avatar, + bucket: 'subjects', + id: subject.id, + }); + } + const res = await upsertSubject( { subjectId: subject?.id }, - { data: values.data, name: values.name }, + { data: values.data, name: values.name, tags: values.tags }, ); if (res.error) { @@ -62,27 +78,18 @@ const SubjectForm = ({ subject }: SubjectFormProps) => { return; } - const supabase = createBrowserSupabaseClient(); - const subjectId = res.data!.id; - - if (!values.avatar) { - await Promise.all([ - supabase.storage.from('subjects').remove([`${subjectId}/avatar`]), - supabase - .from('subjects') - .update({ image_uri: null }) - .eq('id', subjectId), - ]); + if (subject) { + router.back(); + return; } - if (values.avatar instanceof File) { - await supabase.storage - .from('subjects') - .upload(`${subjectId}/avatar`, values.avatar, { upsert: true }); - } + await upsertAvatar({ + avatar: values.avatar, + bucket: 'subjects', + id: res.data!.id, + }); - if (!subject?.id) router.replace(`/subjects/${subjectId}`); - else router.back(); + router.replace(`/subjects/${res.data!.id}`); }), )} > @@ -92,15 +99,15 @@ const SubjectForm = ({ subject }: SubjectFormProps) => { - Profile image - {form.watch('avatar') && ( + Image + {avatar && ( form.setValue('avatar', null)}> Remove image )} form.setValue('avatar', files[0])} /> @@ -164,6 +171,33 @@ const SubjectForm = ({ subject }: SubjectFormProps) => {
+ + Tags + ( + + + + Image + {avatar && ( + form.setValue('avatar', null)}> + Remove image + + )} + form.setValue('avatar', files[0])} + /> + + {form.formState.errors.root && ( +
{form.formState.errors.root.message}
+ )} +
+ + + + +
+ + ); +}; + +export default AccountProfileForm; diff --git a/app/_components/template-form-section.tsx b/app/_components/template-form-section.tsx index a7da3aa0..73648bbd 100644 --- a/app/_components/template-form-section.tsx +++ b/app/_components/template-form-section.tsx @@ -3,7 +3,7 @@ import Input from '@/_components/input'; import InputRoot from '@/_components/input-root'; import * as Label from '@/_components/label'; -import Select, { IOption } from '@/_components/select'; +import Select, { IOption } from '@/_components/select-v1'; import { ListSubjectsByTeamIdData } from '@/_queries/list-subjects-by-team-id'; import * as Form from 'react-hook-form'; import { PropsValue } from 'react-select'; diff --git a/app/_components/toggle-group.tsx b/app/_components/toggle-group.tsx new file mode 100644 index 00000000..b79d9e9c --- /dev/null +++ b/app/_components/toggle-group.tsx @@ -0,0 +1,47 @@ +'use client'; + +import CheckIcon from '@heroicons/react/24/outline/CheckIcon'; +import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'; +import * as React from 'react'; +import { twMerge } from 'tailwind-merge'; + +const Item = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, className, ...props }, ref) => ( + +
+ +
+ {children} +
+)); + +Item.displayName = ToggleGroupPrimitive.Item.displayName; + +const Root = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, className, ...props }, ref) => ( + + {children} + +)); + +Root.displayName = ToggleGroupPrimitive.Root.displayName; + +export { Item, Root }; diff --git a/app/_constants/constant-subscription-variant-id-names.ts b/app/_constants/constant-subscription-variant-id-names.ts new file mode 100644 index 00000000..49ff5690 --- /dev/null +++ b/app/_constants/constant-subscription-variant-id-names.ts @@ -0,0 +1,8 @@ +import SubscriptionVariantName from '@/_constants/enum-subscription-variant-name'; + +const SUBSCRIPTION_VARIANT_ID_NAMES = { + [process.env.LEMON_SQUEEZY_VARIANT_ID_PRO!]: SubscriptionVariantName.Pro, + [process.env.LEMON_SQUEEZY_VARIANT_ID_TEAM!]: SubscriptionVariantName.Team, +}; + +export default SUBSCRIPTION_VARIANT_ID_NAMES; diff --git a/app/_constants/constant-subscription-variant-name-ids.ts b/app/_constants/constant-subscription-variant-name-ids.ts new file mode 100644 index 00000000..932149d8 --- /dev/null +++ b/app/_constants/constant-subscription-variant-name-ids.ts @@ -0,0 +1,8 @@ +import SubscriptionVariantName from '@/_constants/enum-subscription-variant-name'; + +const SUBSCRIPTION_VARIANT_NAME_IDS = { + [SubscriptionVariantName.Pro]: process.env.LEMON_SQUEEZY_VARIANT_ID_PRO!, + [SubscriptionVariantName.Team]: process.env.LEMON_SQUEEZY_VARIANT_ID_TEAM!, +}; + +export default SUBSCRIPTION_VARIANT_NAME_IDS; diff --git a/app/_constants/enum-subscription-variant-name.ts b/app/_constants/enum-subscription-variant-name.ts new file mode 100644 index 00000000..c8b8db32 --- /dev/null +++ b/app/_constants/enum-subscription-variant-name.ts @@ -0,0 +1,6 @@ +enum SubscriptionVariantName { + Pro = 'pro', + Team = 'team', +} + +export default SubscriptionVariantName; diff --git a/app/_constants/enum-team-member-role.ts b/app/_constants/enum-team-member-role.ts new file mode 100644 index 00000000..5e72ee3e --- /dev/null +++ b/app/_constants/enum-team-member-role.ts @@ -0,0 +1,8 @@ +enum TeamMemberRole { + Admin = 'admin', + Owner = 'owner', + Recorder = 'recorder', + Viewer = 'viewer', +} + +export default TeamMemberRole; diff --git a/app/_mutations/create-customer-checkout.ts b/app/_mutations/create-customer-checkout.ts index 5b443de6..03d50724 100644 --- a/app/_mutations/create-customer-checkout.ts +++ b/app/_mutations/create-customer-checkout.ts @@ -1,19 +1,27 @@ 'use server'; +import SUBSCRIPTION_VARIANT_NAME_IDS from '@/_constants/constant-subscription-variant-name-ids'; +import SubscriptionVariantName from '@/_constants/enum-subscription-variant-name'; import getCurrentUser from '@/_queries/get-current-user'; import * as ls from '@lemonsqueezy/lemonsqueezy.js'; -const createCustomerCheckout = async () => { +const createCustomerCheckout = async ({ + teamId, + variant, +}: { + teamId: string; + variant: SubscriptionVariantName; +}) => { const user = await getCurrentUser(); if (!user) return { url: null }; ls.lemonSqueezySetup({ apiKey: process.env.LEMON_SQUEEZY_API_KEY! }); const res = await ls.createCheckout( process.env.LEMON_SQUEEZY_STORE_ID!, - process.env.LEMON_SQUEEZY_VARIANT_ID!, + SUBSCRIPTION_VARIANT_NAME_IDS[variant], { checkoutData: { - custom: { user_id: user.id }, + custom: { team_id: teamId, user_id: user.id }, email: user.email, name: `${user.user_metadata.first_name} ${user.user_metadata.last_name}`, }, diff --git a/app/_mutations/create-tag.ts b/app/_mutations/create-tag.ts new file mode 100644 index 00000000..c9e190b1 --- /dev/null +++ b/app/_mutations/create-tag.ts @@ -0,0 +1,17 @@ +'use server'; + +import getCurrentUser from '@/_queries/get-current-user'; +import createServerSupabaseClient from '@/_utilities/create-server-supabase-client'; + +const createTag = async ({ name }: { name: string }) => { + const supabase = await createServerSupabaseClient(); + const user = await getCurrentUser(); + + return supabase + .from('tags') + .insert({ name, team_id: user?.app_metadata?.active_team_id }) + .select('id') + .single(); +}; + +export default createTag; diff --git a/app/_mutations/set-active-team.ts b/app/_mutations/set-active-team.ts new file mode 100644 index 00000000..8cf576ac --- /dev/null +++ b/app/_mutations/set-active-team.ts @@ -0,0 +1,32 @@ +'use server'; + +import getCurrentUser from '@/_queries/get-current-user'; +import createServerSupabaseClient from '@/_utilities/create-server-supabase-client'; +import { revalidatePath } from 'next/cache'; + +const setActiveTeam = async (teamId: string) => { + const supabase = await createServerSupabaseClient(); + + const { data: team } = await supabase + .from('teams') + .select('id') + .eq('id', teamId) + .single(); + + if (!team) return; + + const supabaseService = await createServerSupabaseClient({ + apiKey: process.env.SUPABASE_SERVICE_KEY!, + }); + + await supabaseService.auth.admin.updateUserById( + (await getCurrentUser())?.id ?? '', + { + app_metadata: { active_team_id: team.id }, + }, + ); + + revalidatePath('/', 'layout'); +}; + +export default setActiveTeam; diff --git a/app/_mutations/sign-up.ts b/app/_mutations/sign-up.ts index f7ecf436..27007983 100644 --- a/app/_mutations/sign-up.ts +++ b/app/_mutations/sign-up.ts @@ -4,6 +4,7 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { Resend } from 'resend'; +import { v4 } from 'uuid'; const signUp = async ( context: { next?: string }, @@ -12,6 +13,7 @@ const signUp = async ( email: string; firstName: string; lastName: string; + organization: string; password: string; }; error: string; @@ -19,39 +21,52 @@ const signUp = async ( data: FormData, ) => { const { get } = await headers(); - const proto = get('x-forwarded-proto'); - const host = get('host'); const email = data.get('email') as string; - const firstName = data.get('firstName') as string; - const lastName = data.get('lastName') as string; + const firstName = (data.get('firstName') as string).trim(); + const host = get('host'); + const isClient = !!context.next?.includes('/join/'); + const lastName = (data.get('lastName') as string).trim(); + const organization = (data.get('organization') as string)?.trim(); const password = data.get('password') as string; - const isClient = context.next?.includes('/join/'); + const proto = get('x-forwarded-proto'); + const teamId = v4(); - const { error } = await ( + const { + data: { user }, + error, + } = await ( await createServerSupabaseClient() ).auth.signUp({ email, options: { data: { first_name: firstName, - is_client: isClient, last_name: lastName, + organization, + team_id: teamId, }, emailRedirectTo: `${proto}://${host}${context.next ? context.next : isClient ? '/subjects' : '/hey'}`, }, password, }); - if (error) { + if (error || !user) { return { - defaultValues: { email, firstName, lastName, password }, - error: error.message, + defaultValues: { email, firstName, lastName, organization, password }, + error: error?.message ?? 'An error occurred', }; } - const resend = new Resend(process.env.RESEND_API_KEY); + await ( + await createServerSupabaseClient({ + apiKey: process.env.SUPABASE_SERVICE_KEY!, + }) + ).auth.admin.updateUserById(user.id, { + app_metadata: { active_team_id: teamId, is_client: isClient }, + user_metadata: { organization: null, team_id: null }, + }); - await resend.emails.send({ + await new Resend(process.env.RESEND_API_KEY).emails.send({ from: 'system@llog.app', html: `
${JSON.stringify(
       {
@@ -59,6 +74,7 @@ const signUp = async (
         firstName,
         isClient,
         lastName,
+        organization,
       },
       null,
       2,
diff --git a/app/_mutations/update-subject.ts b/app/_mutations/update-subject.ts
index 2d253d95..c0eca4e2 100644
--- a/app/_mutations/update-subject.ts
+++ b/app/_mutations/update-subject.ts
@@ -8,8 +8,6 @@ const updateSubject = async (
   subject: Database['public']['Tables']['subjects']['Update'] & { id: string },
 ) => {
   const supabase = await createServerSupabaseClient();
-
-  // get the latest app_metadata for rls validation
   await supabase.auth.refreshSession();
 
   await supabase
@@ -21,6 +19,7 @@ const updateSubject = async (
       public: subject.public,
     })
     .eq('id', subject.id);
+
   revalidatePath('/', 'layout');
 };
 
diff --git a/app/_mutations/upsert-avatar.ts b/app/_mutations/upsert-avatar.ts
new file mode 100644
index 00000000..6fd2ef73
--- /dev/null
+++ b/app/_mutations/upsert-avatar.ts
@@ -0,0 +1,25 @@
+import createBrowserSupabaseClient from '@/_utilities/create-browser-supabase-client';
+
+const upsertAvatar = async ({
+  avatar,
+  bucket,
+  id,
+}: {
+  avatar: File | string | null;
+  bucket: string;
+  id: string;
+}) => {
+  const supabase = createBrowserSupabaseClient();
+
+  if (!avatar) {
+    await supabase.storage.from(bucket).remove([`${id}/avatar`]);
+  }
+
+  if (avatar instanceof File) {
+    await supabase.storage
+      .from(bucket)
+      .upload(`${id}/avatar`, avatar, { upsert: true });
+  }
+};
+
+export default upsertAvatar;
diff --git a/app/_mutations/upsert-event-type-template.ts b/app/_mutations/upsert-event-type-template.ts
index 8baf924e..0ebafb23 100644
--- a/app/_mutations/upsert-event-type-template.ts
+++ b/app/_mutations/upsert-event-type-template.ts
@@ -2,6 +2,7 @@
 
 import { EventTypeTemplateFormValues } from '@/_components/event-type-template-form';
 import TemplateType from '@/_constants/enum-template-type';
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import sanitizeHtml from '@/_utilities/sanitize-html';
 import { revalidatePath } from 'next/cache';
@@ -11,6 +12,7 @@ const upsertEventTypeTemplate = async (
   data: EventTypeTemplateFormValues,
 ) => {
   const supabase = await createServerSupabaseClient();
+  const user = await getCurrentUser();
 
   const { data: template, error } = await supabase
     .from('templates')
@@ -23,6 +25,7 @@ const upsertEventTypeTemplate = async (
       id: context.templateId,
       name: data.name.trim(),
       public: false,
+      team_id: user?.app_metadata?.active_team_id,
       type: TemplateType.EventType,
     })
     .select('id')
diff --git a/app/_mutations/upsert-event.ts b/app/_mutations/upsert-event.ts
index 87a30ecf..cb79f51d 100644
--- a/app/_mutations/upsert-event.ts
+++ b/app/_mutations/upsert-event.ts
@@ -5,8 +5,6 @@ import InputType from '@/_constants/enum-input-type';
 import { GetEventTypeWithInputsAndOptionsData } from '@/_queries/get-event-type-with-inputs-and-options';
 import { Database, Json } from '@/_types/database';
 import DurationInputType from '@/_types/duration-input';
-import MultiSelectInputType from '@/_types/multi-select-input-type';
-import SelectInputType from '@/_types/select-input-type';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import sanitizeHtml from '@/_utilities/sanitize-html';
 import { revalidatePath } from 'next/cache';
@@ -16,7 +14,7 @@ const upsertEvent = async (
     eventId?: string;
     eventTypeId: string;
     eventTypeInputs: NonNullable['inputs'];
-    isMission: boolean;
+    isProtocol: boolean;
     subjectId: string;
   },
   data: EventFormValues,
@@ -48,8 +46,9 @@ const upsertEvent = async (
   }>(
     (acc, input, i) => {
       if (
-        input === '' ||
+        input === undefined ||
         input === null ||
+        input === '' ||
         (Array.isArray(input) && !input.some((v) => v))
       ) {
         return acc;
@@ -88,7 +87,7 @@ const upsertEvent = async (
         }
 
         case InputType.MultiSelect: {
-          (input as MultiSelectInputType).forEach(({ id }, order) =>
+          (input as string[]).forEach((id, order) =>
             acc.eventInputs.push({ ...payload, input_option_id: id, order }),
           );
 
@@ -96,7 +95,7 @@ const upsertEvent = async (
         }
 
         case InputType.Select: {
-          payload.input_option_id = (input as SelectInputType)?.id;
+          payload.input_option_id = input as string;
           acc.eventInputs.push(payload);
           return acc;
         }
diff --git a/app/_mutations/upsert-input.ts b/app/_mutations/upsert-input.ts
index afa14c8e..06f8a70f 100644
--- a/app/_mutations/upsert-input.ts
+++ b/app/_mutations/upsert-input.ts
@@ -2,6 +2,7 @@
 
 import { InputFormValues } from '@/_components/input-form';
 import InputType from '@/_constants/enum-input-type';
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import { revalidatePath } from 'next/cache';
 
@@ -12,6 +13,7 @@ const upsertInput = async (
   data: InputFormValues,
 ): Promise => {
   const supabase = await createServerSupabaseClient();
+  const user = await getCurrentUser();
   const type = data.type.id;
 
   const { data: input, error } = await supabase
@@ -20,6 +22,7 @@ const upsertInput = async (
       id: context.inputId,
       label: data.label.trim(),
       settings: data.settings,
+      team_id: user?.app_metadata?.active_team_id,
       type,
     })
     .select('id, label')
diff --git a/app/_mutations/upsert-module-template.ts b/app/_mutations/upsert-module-template.ts
index b4392934..061a7cc9 100644
--- a/app/_mutations/upsert-module-template.ts
+++ b/app/_mutations/upsert-module-template.ts
@@ -2,6 +2,7 @@
 
 import { ModuleTemplateFormValues } from '@/_components/module-template-form';
 import TemplateType from '@/_constants/enum-template-type';
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import sanitizeHtml from '@/_utilities/sanitize-html';
 import { revalidatePath } from 'next/cache';
@@ -13,6 +14,7 @@ const upsertModuleTemplate = async (
   data: ModuleTemplateFormValues,
 ): Promise => {
   const supabase = await createServerSupabaseClient();
+  const user = await getCurrentUser();
 
   const { data: template, error } = await supabase
     .from('templates')
@@ -25,6 +27,7 @@ const upsertModuleTemplate = async (
       id: context.templateId,
       name: data.name.trim(),
       public: false,
+      team_id: user?.app_metadata?.active_team_id,
       type: TemplateType.Module,
     })
     .select('id')
diff --git a/app/_mutations/upsert-protocol-template.ts b/app/_mutations/upsert-protocol-template.ts
index 391af4a8..7a25c1fc 100644
--- a/app/_mutations/upsert-protocol-template.ts
+++ b/app/_mutations/upsert-protocol-template.ts
@@ -2,6 +2,7 @@
 
 import { ProtocolTemplateFormValues } from '@/_components/protocol-template-form';
 import TemplateType from '@/_constants/enum-template-type';
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import sanitizeHtml from '@/_utilities/sanitize-html';
 import { revalidatePath } from 'next/cache';
@@ -11,6 +12,7 @@ const upsertProtocolTemplate = async (
   data: ProtocolTemplateFormValues,
 ) => {
   const supabase = await createServerSupabaseClient();
+  const user = await getCurrentUser();
 
   const { data: template, error } = await supabase
     .from('templates')
@@ -29,6 +31,7 @@ const upsertProtocolTemplate = async (
       id: context.templateId,
       name: data.name.trim(),
       public: false,
+      team_id: user?.app_metadata?.active_team_id,
       type: TemplateType.Protocol,
     })
     .select('id')
diff --git a/app/_mutations/upsert-session-template.ts b/app/_mutations/upsert-session-template.ts
index 60474d2d..2e695f55 100644
--- a/app/_mutations/upsert-session-template.ts
+++ b/app/_mutations/upsert-session-template.ts
@@ -2,6 +2,7 @@
 
 import { SessionTemplateFormValues } from '@/_components/session-template-form';
 import TemplateType from '@/_constants/enum-template-type';
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import sanitizeHtml from '@/_utilities/sanitize-html';
 import { revalidatePath } from 'next/cache';
@@ -11,6 +12,7 @@ const upsertSessionTemplate = async (
   data: SessionTemplateFormValues,
 ) => {
   const supabase = await createServerSupabaseClient();
+  const user = await getCurrentUser();
 
   const { data: template, error } = await supabase
     .from('templates')
@@ -26,6 +28,7 @@ const upsertSessionTemplate = async (
       id: context.templateId,
       name: data.name.trim(),
       public: false,
+      team_id: user?.app_metadata?.active_team_id,
       type: TemplateType.Session,
     })
     .select('id')
diff --git a/app/_mutations/upsert-subject.ts b/app/_mutations/upsert-subject.ts
index 2e4bf95a..a2186fae 100644
--- a/app/_mutations/upsert-subject.ts
+++ b/app/_mutations/upsert-subject.ts
@@ -1,6 +1,7 @@
 'use server';
 
 import { SubjectFormValues } from '@/_components/subject-form';
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import sanitizeHtml from '@/_utilities/sanitize-html';
 import { revalidatePath } from 'next/cache';
@@ -10,9 +11,8 @@ const upsertSubject = async (
   data: Omit,
 ) => {
   const supabase = await createServerSupabaseClient();
-
-  // get the latest app_metadata for rls validation
   await supabase.auth.refreshSession();
+  const user = await getCurrentUser();
 
   const { data: subject, error } = await supabase
     .from('subjects')
@@ -26,11 +26,23 @@ const upsertSubject = async (
       },
       id: context.subjectId,
       name: data.name.trim(),
+      team_id: user?.app_metadata?.active_team_id,
     })
     .select('id')
     .single();
 
   if (error) return { error: error.message };
+
+  await supabase
+    .from('subject_tags')
+    .delete()
+    .eq('subject_id', subject.id)
+    .not('tag_id', 'in', `(${data.tags.join(',')})`);
+
+  await supabase
+    .from('subject_tags')
+    .upsert(data.tags.map((tag) => ({ subject_id: subject.id, tag_id: tag })));
+
   revalidatePath('/', 'layout');
   return { data: subject };
 };
diff --git a/app/_mutations/upsert-team.ts b/app/_mutations/upsert-team.ts
new file mode 100644
index 00000000..12b9ec3e
--- /dev/null
+++ b/app/_mutations/upsert-team.ts
@@ -0,0 +1,23 @@
+'use server';
+
+import { TeamFormValues } from '@/_components/team-form';
+import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
+import { revalidatePath } from 'next/cache';
+
+const upsertTeam = async (
+  context: { teamId?: string },
+  data: Omit,
+) => {
+  const res = await (
+    await createServerSupabaseClient()
+  ).rpc('upsert_team', {
+    _id: context.teamId,
+    _name: data.name,
+  });
+
+  if (res.error) return { error: res.error.message };
+  if (context.teamId) revalidatePath('/', 'layout');
+  return { data: { id: res.data } };
+};
+
+export default upsertTeam;
diff --git a/app/_queries/count-notifications.ts b/app/_queries/count-notifications.ts
index 9e29f9cb..c3179d4e 100644
--- a/app/_queries/count-notifications.ts
+++ b/app/_queries/count-notifications.ts
@@ -1,8 +1,10 @@
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 
 const countNotifications = async () =>
   (await createServerSupabaseClient())
     .from('notifications')
-    .select('*', { count: 'estimated', head: true });
+    .select('*', { count: 'estimated', head: true })
+    .eq('profile_id', (await getCurrentUser())?.id ?? '');
 
 export default countNotifications;
diff --git a/app/_queries/get-customer-billing-portal.ts b/app/_queries/get-customer-billing-portal.ts
deleted file mode 100644
index 6c4334b7..00000000
--- a/app/_queries/get-customer-billing-portal.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-'use server';
-
-import getCurrentUser from '@/_queries/get-current-user';
-import * as ls from '@lemonsqueezy/lemonsqueezy.js';
-
-const getCustomerBillingPortal = async () => {
-  const user = await getCurrentUser();
-  if (!user) return { url: null };
-  ls.lemonSqueezySetup({ apiKey: process.env.LEMON_SQUEEZY_API_KEY! });
-  const res = await ls.getCustomer(user.app_metadata.customer_id);
-  return { url: res.data?.data?.attributes?.urls?.customer_portal ?? null };
-};
-
-export default getCustomerBillingPortal;
diff --git a/app/_queries/get-input-with-uses.ts b/app/_queries/get-input-with-uses.ts
index ea6291e5..39560817 100644
--- a/app/_queries/get-input-with-uses.ts
+++ b/app/_queries/get-input-with-uses.ts
@@ -9,7 +9,7 @@ const getInputWithUses = async (inputId: string) =>
       label,
       options:input_options(id, label),
       settings,
-      subjects(id),
+      subjects!input_subjects(id),
       type,
       uses:event_types(subject:subjects(id, name, image_uri))`,
     )
diff --git a/app/_queries/get-input.ts b/app/_queries/get-input.ts
index 1a8b12ae..9e4b4cb2 100644
--- a/app/_queries/get-input.ts
+++ b/app/_queries/get-input.ts
@@ -9,7 +9,7 @@ const getInput = async (inputId: string) =>
       label,
       options:input_options(id, label),
       settings,
-      subjects(id),
+      subjects!input_subjects(id),
       type`,
     )
     .eq('id', inputId)
diff --git a/app/_queries/get-subject.ts b/app/_queries/get-subject.ts
index 68de6905..1103ae03 100644
--- a/app/_queries/get-subject.ts
+++ b/app/_queries/get-subject.ts
@@ -3,7 +3,18 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie
 const getSubject = async (subjectId: string) =>
   (await createServerSupabaseClient())
     .from('subjects')
-    .select('archived, data, id, image_uri, name, public, share_code, team_id')
+    .select(
+      `
+      archived,
+      data,
+      id,
+      image_uri,
+      name,
+      public,
+      share_code,
+      tags:subject_tags(tag_id),
+      team_id`,
+    )
     .eq('id', subjectId)
     .eq('deleted', false)
     .single();
diff --git a/app/_queries/get-subscription-billing-portal.ts b/app/_queries/get-subscription-billing-portal.ts
new file mode 100644
index 00000000..23070685
--- /dev/null
+++ b/app/_queries/get-subscription-billing-portal.ts
@@ -0,0 +1,11 @@
+'use server';
+
+import * as ls from '@lemonsqueezy/lemonsqueezy.js';
+
+const getSubscriptionBillingPortal = async (id: number) => {
+  ls.lemonSqueezySetup({ apiKey: process.env.LEMON_SQUEEZY_API_KEY! });
+  const res = await ls.getSubscription(id);
+  return { url: res.data?.data?.attributes?.urls?.customer_portal ?? null };
+};
+
+export default getSubscriptionBillingPortal;
diff --git a/app/_queries/get-team.ts b/app/_queries/get-team.ts
new file mode 100644
index 00000000..2173184b
--- /dev/null
+++ b/app/_queries/get-team.ts
@@ -0,0 +1,14 @@
+import SubscriptionStatus from '@/_constants/enum-subscription-status';
+import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
+
+const getTeam = async (teamId: string) =>
+  (await createServerSupabaseClient())
+    .from('teams')
+    .select('id, image_uri, name, subscriptions(id, profile_id, variant)')
+    .eq('id', teamId)
+    .eq('subscriptions.status', SubscriptionStatus.Active)
+    .single();
+
+export type GetTeamData = Awaited>['data'];
+
+export default getTeam;
diff --git a/app/_queries/get-template-data.ts b/app/_queries/get-template-data.ts
index 2837c1d3..e42468e2 100644
--- a/app/_queries/get-template-data.ts
+++ b/app/_queries/get-template-data.ts
@@ -1,6 +1,6 @@
 import createBrowserSupabaseClient from '@/_utilities/create-browser-supabase-client';
 
-const getTemplateData = (templateId: string) =>
+const getTemplateData = async (templateId: string) =>
   createBrowserSupabaseClient()
     .from('templates')
     .select('data')
diff --git a/app/_queries/get-template.ts b/app/_queries/get-template.ts
index b1f97d70..645a207c 100644
--- a/app/_queries/get-template.ts
+++ b/app/_queries/get-template.ts
@@ -3,7 +3,7 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie
 const getTemplate = async (templateId: string) =>
   (await createServerSupabaseClient())
     .from('templates')
-    .select('data, description, id, name, subjects(id), type')
+    .select('data, description, id, name, subjects!template_subjects(id), type')
     .eq('id', templateId)
     .single();
 
diff --git a/app/_queries/list-inputs-by-subject-id.ts b/app/_queries/list-inputs-by-subject-id.ts
index 268545a2..049d7e27 100644
--- a/app/_queries/list-inputs-by-subject-id.ts
+++ b/app/_queries/list-inputs-by-subject-id.ts
@@ -21,8 +21,8 @@ const listInputsBySubjectId = async (subjectId: string) => {
 
   return supabase
     .from('inputs')
-    .select('id, label, subjects(id, image_uri, name), type')
-    .eq('team_id', (await getCurrentUser())?.id ?? '')
+    .select('id, label, subjects!input_subjects(id, image_uri, name), type')
+    .eq('team_id', (await getCurrentUser())?.app_metadata?.active_team_id ?? '')
     .eq('archived', false)
     .not('id', 'in', `(${blacklist.data.map((is) => is.input_id).join(',')})`)
     .eq('subjects.deleted', false)
diff --git a/app/_queries/list-inputs-with-uses.ts b/app/_queries/list-inputs-with-uses.ts
index 7048d34e..dc0254cc 100644
--- a/app/_queries/list-inputs-with-uses.ts
+++ b/app/_queries/list-inputs-with-uses.ts
@@ -8,11 +8,11 @@ const listInputsWithUses = async () =>
       `
       id,
       label,
-      subjects(id, image_uri, name),
+      subjects!input_subjects(id, image_uri, name),
       type,
       uses:event_types(subject:subjects(id, name))`,
     )
-    .eq('team_id', (await getCurrentUser())?.id ?? '')
+    .eq('team_id', (await getCurrentUser())?.app_metadata?.active_team_id ?? '')
     .eq('archived', false)
     .eq('subjects.deleted', false)
     .not('subjects.archived', 'is', null)
diff --git a/app/_queries/list-inputs.ts b/app/_queries/list-inputs.ts
index 80af29ed..ce0a92da 100644
--- a/app/_queries/list-inputs.ts
+++ b/app/_queries/list-inputs.ts
@@ -4,8 +4,8 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie
 const listInputs = async () =>
   (await createServerSupabaseClient())
     .from('inputs')
-    .select('id, label, subjects(id, image_uri, name), type')
-    .eq('team_id', (await getCurrentUser())?.id ?? '')
+    .select('id, label, subjects!input_subjects(id, image_uri, name), type')
+    .eq('team_id', (await getCurrentUser())?.app_metadata?.active_team_id ?? '')
     .eq('archived', false)
     .eq('subjects.deleted', false)
     .not('subjects.archived', 'is', null)
diff --git a/app/_queries/list-notifications.ts b/app/_queries/list-notifications.ts
index d5ac63c5..042c6877 100644
--- a/app/_queries/list-notifications.ts
+++ b/app/_queries/list-notifications.ts
@@ -1,3 +1,4 @@
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 
 const listNotifications = async () =>
@@ -38,6 +39,7 @@ const listNotifications = async () =>
       subject:subjects(id, image_uri, name),
       type`,
     )
+    .eq('profile_id', (await getCurrentUser())?.id ?? '')
     .order('created_at', { ascending: false })
     .limit(50);
 
diff --git a/app/_queries/list-subject-managers.ts b/app/_queries/list-subject-managers.ts
deleted file mode 100644
index cb596491..00000000
--- a/app/_queries/list-subject-managers.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
-
-const listSubjectManagers = async (subjectId: string) =>
-  (await createServerSupabaseClient())
-    .from('subject_managers')
-    .select('manager:profiles(id, image_uri, first_name)')
-    .eq('subject_id', subjectId)
-    .neq('profile_id', '70045ed0-b03c-46d8-a784-e05c15a770af');
-
-export type ListSubjectManagersData = Awaited<
-  ReturnType
->['data'];
-
-export default listSubjectManagers;
diff --git a/app/_queries/list-subjects-by-team-id.ts b/app/_queries/list-subjects-by-team-id.ts
index c942cbc4..0907d9f8 100644
--- a/app/_queries/list-subjects-by-team-id.ts
+++ b/app/_queries/list-subjects-by-team-id.ts
@@ -5,7 +5,7 @@ const listSubjectsByTeamId = async () =>
   (await createServerSupabaseClient())
     .from('subjects')
     .select('archived, id, image_uri, name, team_id')
-    .eq('team_id', (await getCurrentUser())?.id ?? '')
+    .eq('team_id', (await getCurrentUser())?.app_metadata?.active_team_id ?? '')
     .eq('deleted', false)
     .not('archived', 'is', null)
     .order('name');
diff --git a/app/_queries/list-subjects.ts b/app/_queries/list-subjects.ts
index ff075eb8..485c6988 100644
--- a/app/_queries/list-subjects.ts
+++ b/app/_queries/list-subjects.ts
@@ -1,13 +1,30 @@
+'use server';
+
+import getCurrentUser from '@/_queries/get-current-user';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 
-const listSubjects = async () =>
-  (await createServerSupabaseClient())
+const listSubjects = async () => {
+  const supabase = await createServerSupabaseClient();
+  const user = await getCurrentUser();
+  const userId = user?.id ?? '';
+
+  const { data: clientSubjectIds } = await supabase
+    .from('subject_clients')
+    .select('subject_id')
+    .eq('profile_id', userId);
+
+  const formattedIds = (clientSubjectIds ?? [])
+    .map((subjectClient) => subjectClient.subject_id)
+    .join(',');
+
+  return supabase
     .from('subjects')
     .select('archived, id, image_uri, name, public, share_code, team_id')
-    .not('team_id', 'is', null)
-    .eq('deleted', false)
-    .not('archived', 'is', null)
-    .order('name');
+    .or(
+      `team_id.eq.${user?.app_metadata.active_team_id}, id.in.(${formattedIds})`,
+    )
+    .eq('deleted', false);
+};
 
 export type ListSubjectsData = Awaited>['data'];
 
diff --git a/app/_queries/list-tags.ts b/app/_queries/list-tags.ts
new file mode 100644
index 00000000..54fdaf0d
--- /dev/null
+++ b/app/_queries/list-tags.ts
@@ -0,0 +1,13 @@
+import getCurrentUser from '@/_queries/get-current-user';
+import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
+
+const listTags = async () =>
+  (await createServerSupabaseClient())
+    .from('tags')
+    .select('id, name')
+    .eq('team_id', (await getCurrentUser())?.app_metadata?.active_team_id ?? '')
+    .order('name');
+
+export type ListTagsData = Awaited>['data'];
+
+export default listTags;
diff --git a/app/_queries/list-teams.ts b/app/_queries/list-teams.ts
new file mode 100644
index 00000000..b4a90b80
--- /dev/null
+++ b/app/_queries/list-teams.ts
@@ -0,0 +1,27 @@
+import SubscriptionStatus from '@/_constants/enum-subscription-status';
+import getCurrentUser from '@/_queries/get-current-user';
+import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
+
+const listTeams = async () => {
+  const supabase = await createServerSupabaseClient();
+  const user = await getCurrentUser();
+
+  const { data: teamIds } = await supabase
+    .from('team_members')
+    .select('team_id')
+    .eq('profile_id', user?.id ?? '');
+
+  return supabase
+    .from('teams')
+    .select('id, image_uri, name, subscriptions(id, profile_id, variant)')
+    .in(
+      'id',
+      (teamIds ?? []).map((teamMember) => teamMember.team_id),
+    )
+    .eq('subscriptions.status', SubscriptionStatus.Active)
+    .order('name');
+};
+
+export type ListTeamsData = Awaited>['data'];
+
+export default listTeams;
diff --git a/app/_queries/list-templates-by-subject-id-and-type.ts b/app/_queries/list-templates-by-subject-id-and-type.ts
index d92e2e86..ea3bcf3d 100644
--- a/app/_queries/list-templates-by-subject-id-and-type.ts
+++ b/app/_queries/list-templates-by-subject-id-and-type.ts
@@ -28,8 +28,8 @@ const listTemplatesBySubjectIdAndType = async ({
 
   return supabase
     .from('templates')
-    .select('id, name, subjects(id, image_uri, name), type')
-    .eq('team_id', (await getCurrentUser())?.id ?? '')
+    .select('id, name, subjects!template_subjects(id, image_uri, name), type')
+    .eq('team_id', (await getCurrentUser())?.app_metadata?.active_team_id ?? '')
     .eq('type', type)
     .not(
       'id',
diff --git a/app/_queries/list-templates.ts b/app/_queries/list-templates.ts
index 6258a8d0..dbd7c0d3 100644
--- a/app/_queries/list-templates.ts
+++ b/app/_queries/list-templates.ts
@@ -5,8 +5,11 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie
 const listTemplates = async ({ type }: { type?: TemplateType } = {}) => {
   const q = (await createServerSupabaseClient())
     .from('templates')
-    .select('id, name, subjects(id, image_uri, name), type')
-    .eq('team_id', (await getCurrentUser())?.id ?? '');
+    .select('id, name, subjects!template_subjects(id, image_uri, name), type')
+    .eq(
+      'team_id',
+      (await getCurrentUser())?.app_metadata?.active_team_id ?? '',
+    );
 
   if (type) q.eq('type', type);
   else q.not('type', 'is', null);
diff --git a/app/_types/multi-select-input-type.ts b/app/_types/multi-select-input-type.ts
deleted file mode 100644
index 8c4db243..00000000
--- a/app/_types/multi-select-input-type.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-type MultiSelectInputType = Array<{ id: string; label: string }>;
-
-export default MultiSelectInputType;
diff --git a/app/_types/select-input-type.ts b/app/_types/select-input-type.ts
deleted file mode 100644
index bcf6a532..00000000
--- a/app/_types/select-input-type.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-type SelectInputType = { id: string; label: string } | null;
-
-export default SelectInputType;
diff --git a/app/_utilities/get-insight-options-from-events.ts b/app/_utilities/get-insight-options-from-events.ts
index d3824c5f..e9f86c5d 100644
--- a/app/_utilities/get-insight-options-from-events.ts
+++ b/app/_utilities/get-insight-options-from-events.ts
@@ -1,4 +1,4 @@
-import { IOption } from '@/_components/select';
+import { IOption } from '@/_components/select-v1';
 import InputType from '@/_constants/enum-input-type';
 import { ListEventsData } from '@/_queries/list-events';
 import firstIfArray from '@/_utilities/first-if-array';
diff --git a/app/api/lemon-squeezy/webhook/route.ts b/app/api/lemon-squeezy/webhook/route.ts
index e21de2a4..aa86ab0d 100644
--- a/app/api/lemon-squeezy/webhook/route.ts
+++ b/app/api/lemon-squeezy/webhook/route.ts
@@ -1,3 +1,4 @@
+import SUBSCRIPTION_VARIANT_ID_NAMES from '@/_constants/constant-subscription-variant-id-names';
 import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';
 import crypto from 'crypto';
 
@@ -15,30 +16,47 @@ const POST = async (request: Request) => {
     return new Response('Invalid signature', { status: 400 });
   }
 
-  let customerId, eventName, subscriptionStatus, userId;
+  let customerId,
+    eventName,
+    subscriptionId,
+    subscriptionStatus,
+    teamId,
+    userId,
+    variantId;
 
   try {
     const json = JSON.parse(body);
     customerId = json.data.attributes.customer_id;
     eventName = json.meta.event_name;
+    subscriptionId = json.data.attributes.first_subscription_item.id;
     subscriptionStatus = json.data.attributes.status;
+    teamId = json.meta.custom_data.team_id;
     userId = json.meta.custom_data.user_id;
+    variantId = json.data.attributes.variant_id;
   } catch {
     return new Response('Invalid payload', { status: 400 });
   }
 
   switch (eventName) {
     case 'subscription_created':
-    case 'subscription_expired': {
-      await (
-        await createServerSupabaseClient({
-          apiKey: process.env.SUPABASE_SERVICE_KEY!,
-        })
-      ).auth.admin.updateUserById(userId, {
-        app_metadata: {
-          customer_id: customerId,
-          subscription_status: subscriptionStatus,
-        },
+    case 'subscription_updated': {
+      const supabase = await createServerSupabaseClient({
+        apiKey: process.env.SUPABASE_SERVICE_KEY!,
+      });
+
+      await supabase.auth.admin.updateUserById(userId, {
+        app_metadata: { customer_id: customerId },
+      });
+
+      await supabase.from('subscriptions').upsert({
+        id: subscriptionId,
+        profile_id: userId,
+        status: subscriptionStatus,
+
+        // default to user_id because kelsie doesn't have team_id in her sub
+        team_id: teamId ?? userId,
+
+        variant: SUBSCRIPTION_VARIANT_ID_NAMES[variantId],
       });
 
       return new Response('OK', { status: 200 });
diff --git a/bun.lockb b/bun.lockb
index 5163ca26..b319e995 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/middleware.ts b/middleware.ts
index 01d96b77..38bac458 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -49,7 +49,6 @@ export const middleware = async (req: NextRequest) => {
       '/inputs',
       '/subjects',
       '/templates',
-      '/upgrade',
     ];
 
     if (
diff --git a/package.json b/package.json
index 6b7f1b78..89c3bffa 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
     "format": "prettier --write .",
     "lint": "next lint",
     "start": "next build && next start",
-    "up": "bun update --latest && bun i eslint@8.57.0 next@canary react@canary react-dom@canary"
+    "up": "bun update --latest && bun i eslint@8.57.0 react@rc react-dom@rc"
   },
   "dependencies": {
     "@dnd-kit/core": "^6.1.0",
@@ -27,60 +27,64 @@
     "@radix-ui/react-dropdown-menu": "^2.1.2",
     "@radix-ui/react-label": "^2.1.0",
     "@radix-ui/react-popover": "^1.1.2",
+    "@radix-ui/react-select": "^2.1.2",
     "@radix-ui/react-slot": "^1.1.0",
     "@radix-ui/react-switch": "^1.1.1",
+    "@radix-ui/react-toggle-group": "^1.1.0",
     "@supabase/ssr": "^0.5.1",
-    "@supabase/supabase-js": "^2.45.4",
+    "@supabase/supabase-js": "^2.45.6",
     "@tailwindcss/typography": "^0.5.15",
-    "@tiptap/extension-bold": "^2.8.0",
-    "@tiptap/extension-bullet-list": "^2.8.0",
-    "@tiptap/extension-document": "^2.8.0",
-    "@tiptap/extension-history": "^2.8.0",
-    "@tiptap/extension-italic": "^2.8.0",
-    "@tiptap/extension-link": "^2.8.0",
-    "@tiptap/extension-list-item": "^2.8.0",
-    "@tiptap/extension-ordered-list": "^2.8.0",
-    "@tiptap/extension-paragraph": "^2.8.0",
-    "@tiptap/extension-placeholder": "^2.8.0",
-    "@tiptap/extension-text": "^2.8.0",
-    "@tiptap/extension-typography": "^2.8.0",
-    "@tiptap/extension-underline": "^2.8.0",
-    "@tiptap/extension-youtube": "^2.8.0",
-    "@tiptap/react": "^2.8.0",
+    "@tiptap/extension-bold": "^2.9.1",
+    "@tiptap/extension-bullet-list": "^2.9.1",
+    "@tiptap/extension-document": "^2.9.1",
+    "@tiptap/extension-history": "^2.9.1",
+    "@tiptap/extension-italic": "^2.9.1",
+    "@tiptap/extension-link": "^2.9.1",
+    "@tiptap/extension-list-item": "^2.9.1",
+    "@tiptap/extension-ordered-list": "^2.9.1",
+    "@tiptap/extension-paragraph": "^2.9.1",
+    "@tiptap/extension-placeholder": "^2.9.1",
+    "@tiptap/extension-text": "^2.9.1",
+    "@tiptap/extension-typography": "^2.9.1",
+    "@tiptap/extension-underline": "^2.9.1",
+    "@tiptap/extension-youtube": "^2.9.1",
+    "@tiptap/react": "^2.9.1",
     "@types/humanize-duration": "^3.27.4",
-    "@types/lodash": "^4.17.10",
-    "@types/react": "^18.3.11",
-    "@types/react-dom": "^18.3.0",
-    "@typescript-eslint/eslint-plugin": "^8.8.1",
-    "@typescript-eslint/parser": "^8.8.1",
+    "@types/lodash": "^4.17.12",
+    "@types/react": "^18.3.12",
+    "@types/react-dom": "^18.3.1",
+    "@types/uuid": "^10.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.11.0",
+    "@typescript-eslint/parser": "^8.11.0",
     "@uidotdev/usehooks": "^2.4.1",
-    "@vercel/analytics": "^1.3.1",
+    "@vercel/analytics": "^1.3.2",
     "@visx/responsive": "^3.10.2",
     "autoprefixer": "^10.4.20",
     "eslint": "8.57.0",
-    "eslint-config-next": "^14.2.15",
+    "eslint-config-next": "^15.0.1",
     "eslint-config-prettier": "^9.1.0",
     "fuse.js": "^7.0.0",
     "humanize-duration": "^3.32.1",
     "json-2-csv": "^5.5.6",
     "lodash": "^4.17.21",
     "nanoid": "^5.0.7",
-    "next": "^15.0.0-canary.179",
+    "next": "^15.0.1",
     "prettier": "^3.3.3",
     "prettier-plugin-organize-imports": "^4.1.0",
     "prettier-plugin-tailwindcss": "^0.6.8",
-    "react": "^19.0.0-rc-ed966dac-20241007",
-    "react-dom": "^19.0.0-rc-ed966dac-20241007",
-    "react-dropzone": "^14.2.9",
-    "react-hook-form": "7.53.0",
-    "react-select": "^5.8.1",
+    "react": "^19.0.0-rc-cae764ce-20241025",
+    "react-dom": "^19.0.0-rc-cae764ce-20241025",
+    "react-dropzone": "^14.2.10",
+    "react-hook-form": "7.53.1",
+    "react-select": "^5.8.2",
     "resend": "^4.0.0",
-    "supabase": "^1.203.0",
-    "tailwind-merge": "^2.5.3",
-    "tailwindcss": "^3.4.13",
+    "supabase": "^1.207.9",
+    "tailwind-merge": "^2.5.4",
+    "tailwindcss": "^3.4.14",
     "typescript": "^5.6.3",
-    "vaul": "^1.0.0",
-    "vercel": "^37.6.3",
+    "uuid": "^11.0.1",
+    "vaul": "^1.1.0",
+    "vercel": "^37.12.1",
     "xss": "^1.0.15"
   }
 }
diff --git a/supabase/migrations/20240913020338_rename-training-plans-to-protocols.sql b/supabase/migrations/20240913020338_rename-training-plans-to-protocols.sql
index 6e417c84..2f43e3aa 100644
--- a/supabase/migrations/20240913020338_rename-training-plans-to-protocols.sql
+++ b/supabase/migrations/20240913020338_rename-training-plans-to-protocols.sql
@@ -189,11 +189,11 @@ create or replace function public.get_public_protocol_with_sessions_and_events(p
           'modules', coalesce((
             select json_agg(json_build_object(
               'id', et.id,
-              'event', (
+              'event', coalesce((
                 select json_agg(json_build_object('created_at', e.created_at, 'id', e.id))
                 from events e
                 where e.event_type_id = et.id
-              )
+              ), '[]'::json)
             ) order by et.order)
             from event_types et
             where et.archived = false and et.session_id = s.id
diff --git a/supabase/migrations/20241011171810_add-team-functionality.sql b/supabase/migrations/20241011171810_add-team-functionality.sql
new file mode 100644
index 00000000..f5ac31cf
--- /dev/null
+++ b/supabase/migrations/20241011171810_add-team-functionality.sql
@@ -0,0 +1,1658 @@
+-- add enums
+
+create type team_member_role as enum ('owner', 'admin', 'recorder', 'viewer');
+
+-- add tags
+
+create table "public"."input_tags" (
+  "input_id" uuid not null,
+  "tag_id" uuid not null
+);
+
+create table "public"."subject_tags" (
+  "subject_id" uuid not null,
+  "tag_id" uuid not null
+);
+
+create table "public"."tags" (
+  "id" uuid not null default gen_random_uuid (),
+  "name" text not null,
+  "team_id" uuid not null
+);
+
+create table "public"."team_member_subject_roles" (
+  "team_id" uuid not null,
+  "profile_id" uuid not null,
+  "subject_id" uuid not null,
+  "role" team_member_role not null
+);
+
+create table "public"."team_member_tag_roles" (
+  "team_id" uuid not null,
+  "profile_id" uuid not null,
+  "tag_id" uuid not null,
+  "role" team_member_role not null
+);
+
+create table "public"."template_tags" (
+  "template_id" uuid not null,
+  "tag_id" uuid not null
+);
+
+alter table "public"."input_tags" enable row level security;
+alter table "public"."subject_tags" enable row level security;
+alter table "public"."tags" enable row level security;
+alter table "public"."team_member_subject_roles" enable row level security;
+alter table "public"."team_member_tag_roles" enable row level security;
+alter table "public"."template_tags" enable row level security;
+
+create unique index input_tags_pkey on public.input_tags using btree (input_id, tag_id);
+create unique index subject_tags_pkey on public.subject_tags using btree (subject_id, tag_id);
+create unique index tags_pkey on public.tags using btree (id);
+create unique index team_member_subject_roles_pkey on public.team_member_subject_roles using btree (team_id, profile_id, subject_id);
+create unique index team_member_tag_roles_pkey on public.team_member_tag_roles using btree (team_id, profile_id, tag_id);
+create unique index template_tags_pkey on public.template_tags using btree (template_id, tag_id);
+
+create index tags_team_id_name_index on public.tags using btree (team_id, name);
+
+alter table "public"."input_tags" add constraint "input_tags_input_id_fkey" foreign key (input_id) references inputs (id) on update cascade on delete cascade not valid;
+alter table "public"."input_tags" add constraint "input_tags_pkey" primary key using index "input_tags_pkey";
+alter table "public"."input_tags" add constraint "input_tags_tag_id_fkey" foreign key (tag_id) references tags (id) on update cascade on delete cascade not valid;
+alter table "public"."input_tags" add constraint "input_tags_template_id_fkey" foreign key (input_id) references subjects (id) on update cascade on delete cascade not valid;
+alter table "public"."input_tags" add constraint "input_tags_template_id_fkey1" foreign key (input_id) references templates (id) on update cascade on delete cascade not valid;
+alter table "public"."input_tags" validate constraint "input_tags_input_id_fkey";
+alter table "public"."input_tags" validate constraint "input_tags_tag_id_fkey";
+alter table "public"."input_tags" validate constraint "input_tags_template_id_fkey";
+alter table "public"."input_tags" validate constraint "input_tags_template_id_fkey1";
+alter table "public"."subject_tags" add constraint "subject_tags_pkey" primary key using index "subject_tags_pkey";
+alter table "public"."subject_tags" add constraint "subject_tags_subject_id_fkey" foreign key (subject_id) references subjects (id) on update cascade on delete cascade not valid;
+alter table "public"."subject_tags" add constraint "subject_tags_tag_id_fkey" foreign key (tag_id) references tags (id) on update cascade on delete cascade not valid;
+alter table "public"."subject_tags" validate constraint "subject_tags_subject_id_fkey";
+alter table "public"."subject_tags" validate constraint "subject_tags_tag_id_fkey";
+alter table "public"."tags" add constraint "tags_name_length" check (((length(name) > 0) and (length(name) < 50))) not valid;
+alter table "public"."tags" add constraint "tags_pkey" primary key using index "tags_pkey";
+alter table "public"."tags" add constraint "tags_team_id_fkey" foreign key (team_id) references teams (id) on update cascade on delete cascade not valid;
+alter table "public"."tags" validate constraint "tags_name_length";
+alter table "public"."tags" validate constraint "tags_team_id_fkey";
+alter table "public"."team_member_subject_roles" add constraint "team_member_subject_roles_pkey" primary key using index "team_member_subject_roles_pkey";
+alter table "public"."team_member_subject_roles" add constraint "team_member_subject_roles_profile_id_fkey" foreign key (profile_id) references profiles (id) on update cascade on delete cascade not valid;
+alter table "public"."team_member_subject_roles" add constraint "team_member_subject_roles_tag_id_fkey" foreign key (subject_id) references subjects (id) on update cascade on delete cascade not valid;
+alter table "public"."team_member_subject_roles" add constraint "team_member_subject_roles_team_id_fkey" foreign key (team_id) references teams (id) on update cascade on delete cascade not valid;
+alter table "public"."team_member_subject_roles" validate constraint "team_member_subject_roles_profile_id_fkey";
+alter table "public"."team_member_subject_roles" validate constraint "team_member_subject_roles_tag_id_fkey";
+alter table "public"."team_member_subject_roles" validate constraint "team_member_subject_roles_team_id_fkey";
+alter table "public"."team_member_tag_roles" add constraint "team_member_tag_roles_pkey" primary key using index "team_member_tag_roles_pkey";
+alter table "public"."team_member_tag_roles" add constraint "team_member_tag_roles_profile_id_fkey" foreign key (profile_id) references profiles (id) on update cascade on delete cascade not valid;
+alter table "public"."team_member_tag_roles" add constraint "team_member_tag_roles_tag_id_fkey" foreign key (tag_id) references tags (id) on update cascade on delete cascade not valid;
+alter table "public"."team_member_tag_roles" add constraint "team_member_tag_roles_team_id_fkey" foreign key (team_id) references teams (id) on update cascade on delete cascade not valid;
+alter table "public"."team_member_tag_roles" validate constraint "team_member_tag_roles_profile_id_fkey";
+alter table "public"."team_member_tag_roles" validate constraint "team_member_tag_roles_tag_id_fkey";
+alter table "public"."team_member_tag_roles" validate constraint "team_member_tag_roles_team_id_fkey";
+alter table "public"."template_tags" add constraint "template_tags_pkey" primary key using index "template_tags_pkey";
+alter table "public"."template_tags" add constraint "template_tags_subject_id_fkey" foreign key (template_id) references subjects (id) on update cascade on delete cascade not valid;
+alter table "public"."template_tags" add constraint "template_tags_tag_id_fkey" foreign key (tag_id) references tags (id) on update cascade on delete cascade not valid;
+alter table "public"."template_tags" add constraint "template_tags_template_id_fkey" foreign key (template_id) references templates (id) on update cascade on delete cascade not valid;
+alter table "public"."template_tags" validate constraint "template_tags_subject_id_fkey";
+alter table "public"."template_tags" validate constraint "template_tags_tag_id_fkey";
+alter table "public"."template_tags" validate constraint "template_tags_template_id_fkey";
+
+-- add subscriptions
+
+create type "public"."subscription_variant" as enum ('pro', 'team');
+
+create table "public"."subscriptions" (
+  "id" bigint not null,
+  "team_id" uuid not null,
+  "profile_id" uuid not null,
+  "variant" subscription_variant not null,
+  "status" subscription_status not null
+);
+
+alter table "public"."subscriptions" enable row level security;
+create unique index subscriptions_pkey on public.subscriptions using btree (id);
+create index subscriptions_team_id_status_variant_index on public.subscriptions using btree (team_id, status, variant);
+alter table "public"."subscriptions" add constraint "subscriptions_pkey" primary key using index "subscriptions_pkey";
+alter table "public"."subscriptions" add constraint "subscriptions_profile_id_fkey" foreign key (profile_id) references profiles (id) on update cascade on delete cascade not valid;
+alter table "public"."subscriptions" validate constraint "subscriptions_profile_id_fkey";
+alter table "public"."subscriptions" add constraint "subscriptions_team_id_fkey" foreign key (team_id) references teams (id) on update cascade on delete cascade not valid;
+alter table "public"."subscriptions" validate constraint "subscriptions_team_id_fkey";
+
+-- alter tables
+
+alter table "public"."inputs" alter column "team_id" drop default;
+alter table "public"."subjects" alter column "team_id" drop default;
+alter table "public"."team_members" alter column "team_id" drop default;
+alter table "public"."templates" alter column "team_id" drop default;
+alter table "public"."teams" add column "image_uri" text;
+
+alter table "public"."team_members" add column "role" team_member_role;
+update public.team_members set role = 'owner';
+alter table "public"."team_members" alter column "role" set not null;
+
+-- update functions
+
+create or replace function public.handle_insert_user()
+  returns trigger
+  language plpgsql
+  security definer
+  as $$
+  declare
+    new_team_id uuid;
+  begin
+    insert into public.profiles (id, first_name, last_name)
+      values (new.id, new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data ->> 'last_name');
+    insert into public.teams (id, name) values ((new.raw_user_meta_data ->> 'team_id')::uuid, coalesce(nullif(new.raw_user_meta_data ->> 'organization', ''), 'Personal')) returning id into new_team_id;
+    insert into public.team_members (team_id, profile_id, role) values (new_team_id, new.id, 'owner');
+    return new;
+  end;
+  $$;
+
+create or replace function public.is_client()
+  returns boolean
+  language plpgsql
+  as $$
+  begin
+    return coalesce((select auth.jwt() -> 'app_metadata' ->> 'is_client')::boolean, false);
+  end;
+  $$;
+
+create or replace function public.can_insert_subject_on_current_plan (subject_id uuid default null)
+  returns boolean
+  language plpgsql
+  as $$
+  declare
+    subject_count int;
+    subscription_status text;
+  begin
+    select count(*)
+      into subject_count
+      from subjects s
+      where s.team_id = (select auth.jwt() -> 'app_metadata' ->> 'active_team_id')::uuid
+        and s.deleted = false
+        and s.archived = false
+        and s.id is distinct from subject_id;
+    select (select auth.jwt() -> 'app_metadata' ->> 'subscription_status')
+      into subscription_status;
+    return (subject_count < 2 or subscription_status = 'active') is true;
+  end;
+  $$;
+
+create or replace function public.handle_insert_or_update_object ()
+  returns trigger
+  language plpgsql
+  security definer
+  set search_path to 'public'
+  as $$
+  begin
+    if (new.bucket_id = 'teams' and storage.filename (new.name) = 'avatar') then
+      update public.teams
+        set image_uri = new.bucket_id || '/' || new.name || '?c=' || substr(md5(random()::text), 0, 5)
+        where id::text = (storage.foldername (new.name))[1];
+    end if;
+    if (new.bucket_id = 'subjects' and storage.filename (new.name) = 'avatar') then
+      update public.subjects
+        set image_uri = new.bucket_id || '/' || new.name || '?c=' || substr(md5(random()::text), 0, 5)
+        where id::text = (storage.foldername (new.name))[1];
+    end if;
+    if (new.bucket_id = 'profiles' and storage.filename (new.name) = 'avatar') then
+      update auth.users
+        set raw_user_meta_data = jsonb_set(raw_user_meta_data, '{image_uri}', to_jsonb (new.bucket_id || '/' || new.name || '?c=' || substr(md5(random()::text), 0, 5)))
+        where id::text = (storage.foldername (new.name))[1];
+    end if;
+    return new;
+  end;
+  $$;
+
+create or replace function public.get_public_protocol_with_sessions_and_events(public_protocol_id uuid)
+  returns json
+  language plpgsql
+  security definer
+  as $$
+  declare
+    result json;
+  begin
+    select json_build_object(
+      'id', p.id,
+      'name', p.name,
+      'sessions', coalesce((
+        select json_agg(json_build_object(
+          'draft', s.draft,
+          'id', s.id,
+          'order', s.order,
+          'scheduled_for', s.scheduled_for,
+          'title', s.title,
+          'modules', coalesce((
+            select json_agg(json_build_object(
+              'id', et.id,
+              'event', coalesce((
+                select json_agg(json_build_object('created_at', e.created_at, 'id', e.id))
+                from events e
+                where e.event_type_id = et.id
+              ), '[]'::json)
+            ) order by et.order)
+            from event_types et
+            where et.archived = false and et.session_id = s.id
+          ), '[]'::json)
+        ) order by s.order)
+        from sessions s
+        where s.protocol_id = p.id and s.draft = false
+      ), '[]'::json)
+    )
+    into result
+    from protocols p
+    join subjects sub on p.subject_id = sub.id
+    where p.id = public_protocol_id and sub.public = true
+    order by p.id;
+    return result;
+  end;
+  $$;
+
+create or replace function public.handle_delete_object ()
+  returns trigger
+  language plpgsql
+  security definer
+  set search_path to 'public'
+  as $$
+  begin
+    if (old.bucket_id = 'teams' and storage.filename (old.name) = 'avatar') then
+      update public.teams
+        set image_uri = null
+        where id::text = (storage.foldername (old.name))[1];
+    end if;
+    if (old.bucket_id = 'subjects' and storage.filename (old.name) = 'avatar') then
+      update public.subjects
+        set image_uri = null
+        where id::text = (storage.foldername (old.name))[1];
+    end if;
+    if (old.bucket_id = 'profiles' and storage.filename (old.name) = 'avatar') then
+      update auth.users
+        set raw_user_meta_data = jsonb_set(raw_user_meta_data, '{image_uri}', 'null'::jsonb)
+        where id::text = (storage.foldername (old.name))[1];
+    end if;
+    return old;
+  end;
+  $$;
+
+create trigger on_delete_object
+  after delete on storage.objects
+  for each row
+  execute function handle_delete_object ();
+
+-- add functions
+
+create or replace function public.upsert_team(_name text, _id uuid default null)
+  returns uuid
+  language plpgsql
+  set search_path = ''
+  as $$
+  begin
+    if _id is null then
+      insert into public.teams (name) values (_name) returning id into _id;
+      insert into public.team_members (team_id, profile_id, role) values (_id, (select auth.uid()::uuid), 'owner'::public.team_member_role);
+    else
+      update public.teams set name = _name where id = _id;
+    end if;
+    return _id;
+  end;
+  $$;
+
+-- nuke rls
+
+drop policy "Owners and team members can delete." on "public"."comments";
+drop policy "Team members & subject managers can insert." on "public"."comments";
+drop policy "Team members & subject managers can select." on "public"."comments";
+drop policy "Team members & subject managers can delete." on "public"."event_inputs";
+drop policy "Team members & subject managers can insert." on "public"."event_inputs";
+drop policy "Team members & subject managers can select." on "public"."event_inputs";
+drop policy "Team members & subject managers can update." on "public"."event_inputs";
+drop policy "Team members & subject managers can select." on "public"."event_type_inputs";
+drop policy "Team members can delete." on "public"."event_type_inputs";
+drop policy "Team members can insert." on "public"."event_type_inputs";
+drop policy "Team members & subject managers can select." on "public"."event_types";
+drop policy "Team members can delete." on "public"."event_types";
+drop policy "Team members can insert." on "public"."event_types";
+drop policy "Team members can update." on "public"."event_types";
+drop policy "Team members & subject managers can insert." on "public"."events";
+drop policy "Team members & subject managers can select." on "public"."events";
+drop policy "Team members & subject managers can update." on "public"."events";
+drop policy "Team members can delete." on "public"."events";
+drop policy "Team members & subject managers can insert." on "public"."input_options";
+drop policy "Team members & subject managers can select." on "public"."input_options";
+drop policy "Team members can delete." on "public"."input_options";
+drop policy "Team members can update." on "public"."input_options";
+drop policy "Team members & subject managers can select." on "public"."input_subjects";
+drop policy "Team members can delete." on "public"."input_subjects";
+drop policy "Team members can insert." on "public"."input_subjects";
+drop policy "Team members & subject managers can insert." on "public"."inputs";
+drop policy "Team members & subject managers can select." on "public"."inputs";
+drop policy "Team members can delete." on "public"."inputs";
+drop policy "Team members can update." on "public"."inputs";
+drop policy "Team members & subject managers can select." on "public"."insights";
+drop policy "Team members can delete." on "public"."insights";
+drop policy "Team members can insert." on "public"."insights";
+drop policy "Team members can update." on "public"."insights";
+drop policy "Owners can delete." on "public"."notifications";
+drop policy "Owners can select." on "public"."notifications";
+drop policy "Owners can update." on "public"."notifications";
+drop policy "Authenticated users can select." on "public"."profiles";
+drop policy "Team members & subject managers can select." on "public"."protocols";
+drop policy "Team members can delete." on "public"."protocols";
+drop policy "Team members can insert." on "public"."protocols";
+drop policy "Team members can update." on "public"."protocols";
+drop policy "Team members & subject managers can select." on "public"."sessions";
+drop policy "Team members can delete." on "public"."sessions";
+drop policy "Team members can insert." on "public"."sessions";
+drop policy "Team members can update." on "public"."sessions";
+drop policy "Authenticated users can select." on "public"."subject_managers";
+drop policy "Team members can delete." on "public"."subject_managers";
+drop policy "Team members = full access." on "public"."subject_notes";
+drop policy "Team members & subject managers can select." on "public"."subjects";
+drop policy "Team members can insert." on "public"."subjects";
+drop policy "Team members can update." on "public"."subjects";
+drop policy "Authenticated users can select." on "public"."team_members";
+drop policy "Select template = full access." on "public"."template_subjects";
+drop policy "Team members = full access." on "public"."templates";
+drop policy "Team members & subject managers can select (subjects)." on "storage"."objects";
+drop policy "Team members can delete (subjects)." on "storage"."objects";
+drop policy "Team members can insert (subjects)." on "storage"."objects";
+drop policy "Team members can update (subjects)." on "storage"."objects";
+drop policy "Everyone can select (profiles)." on "storage"."objects";
+drop policy "Owners can delete (profiles)." on "storage"."objects";
+drop policy "Owners can insert (profiles)." on "storage"."objects";
+drop policy "Owners can update (profiles)." on "storage"."objects";
+
+-- subject manager -> subject client
+
+create table "public"."subject_clients" ("profile_id" uuid not null, "subject_id" uuid not null);
+insert into "public"."subject_clients" select * from "public"."subject_managers";
+alter table "public"."subject_clients" enable row level security;
+create unique index subject_clients_pkey on public.subject_clients using btree (profile_id, subject_id);
+create index subject_clients_subject_id_index on public.subject_clients using btree (subject_id);
+alter table "public"."subject_clients" add constraint "subject_clients_pkey" primary key using index "subject_clients_pkey";
+alter table "public"."subject_clients" add constraint "subject_clients_profile_id_fkey" foreign key (profile_id) references profiles(id) on delete cascade not valid;
+alter table "public"."subject_clients" validate constraint "subject_clients_profile_id_fkey";
+alter table "public"."subject_clients" add constraint "subject_clients_subject_id_fkey" foreign key (subject_id) references subjects(id) on delete cascade not valid;
+alter table "public"."subject_clients" validate constraint "subject_clients_subject_id_fkey";
+
+create or replace function public.join_subject_as_client (share_code text)
+  returns void
+  language plpgsql
+  security definer
+  as $$
+  # variable_conflict use_variable
+  declare
+    existing_client boolean;
+    profile_id uuid;
+    subject_id uuid;
+    team_member_profile_ids uuid[];
+  begin
+    select id into subject_id
+      from subjects
+      where subjects.share_code = share_code;
+    if subject_id is not null then
+      select exists(select true from subject_clients
+        where profile_id = auth.uid() and subject_id = subject_id)
+        into existing_client;
+      if not existing_client then
+        insert into subject_clients (profile_id, subject_id)
+          values (auth.uid (), subject_id);
+        select array_agg(tm.profile_id) into team_member_profile_ids
+          from subjects s join team_members tm on s.team_id = tm.team_id
+          where s.id = subject_id;
+        for profile_id in (select unnest(team_member_profile_ids)) loop
+          insert into notifications (profile_id, type, source_profile_id, source_subject_id)
+            values (profile_id, 'join_subject', auth.uid (), subject_id);
+        end loop;
+      end if;
+    end if;
+  end;
+  $$;
+
+create or replace function public.handle_insert_event ()
+  returns trigger
+  language plpgsql
+  security definer
+  as $$
+  declare
+    curr_session_id uuid;
+    profile_id uuid;
+    subject_client_profile_ids uuid[];
+    team_member_profile_ids uuid[];
+  begin
+    select array_agg(sc.profile_id) into subject_client_profile_ids
+      from subject_clients sc
+      where sc.subject_id = new.subject_id and sc.profile_id <> new.profile_id;
+    select array_agg(tm.profile_id) into team_member_profile_ids
+      from subjects s join team_members tm on s.team_id = tm.team_id
+      where s.id = new.subject_id and tm.profile_id <> new.profile_id;
+    select et.session_id into curr_session_id
+      from event_types et
+      where et.id = new.event_type_id;
+    for profile_id in (
+      select unnest(array_cat(subject_client_profile_ids, team_member_profile_ids))) loop
+        if not exists (
+          select 1
+            from notifications n
+            join events e on n.source_event_id = e.id
+            join event_types et on e.event_type_id = et.id
+              where et.session_id = curr_session_id)
+        then
+          insert into notifications (profile_id, type, source_event_id, source_profile_id, source_subject_id)
+            values (profile_id, 'event', new.id, new.profile_id, new.subject_id);
+        end if;
+      end loop;
+    return null;
+  end;
+  $$;
+
+create or replace function public.handle_insert_comment ()
+  returns trigger
+  language plpgsql
+  security definer
+  as $$
+  declare
+    source_subject_id uuid;
+    subject_client_profile_ids uuid[];
+    team_member_profile_ids uuid[];
+    profile_id uuid;
+  begin
+    select e.subject_id into source_subject_id
+      from events e
+      where e.id = new.event_id;
+    select array_agg(sc.profile_id) into subject_client_profile_ids
+      from subject_clients sc
+      where sc.subject_id = source_subject_id and sc.profile_id <> new.profile_id;
+    select array_agg(tm.profile_id) into team_member_profile_ids
+      from subjects s join team_members tm on s.team_id = tm.team_id
+      where s.id = source_subject_id and tm.profile_id <> new.profile_id;
+    for profile_id in (
+      select unnest(array_cat(subject_client_profile_ids, team_member_profile_ids))) loop
+        insert into notifications (profile_id, type, source_comment_id, source_profile_id, source_subject_id)
+          values (profile_id, 'comment', new.id, new.profile_id, source_subject_id);
+      end loop;
+    return null;
+    end;
+  $$;
+
+create or replace function public.list_public_events(
+    public_subject_id uuid,
+    from_arg int default 0,
+    to_arg int default 10000,
+    start_date timestamp without time zone default NULL,
+    end_date timestamp without time zone default NULL
+  )
+    returns json
+    language plpgsql
+    security definer
+    as $$
+  declare
+    result json;
+    limit_count int;
+  begin
+    limit_count := to_arg - from_arg + 1;
+    select json_agg(event_info)
+    into result
+    from (
+      select
+        json_build_object(
+          'id', e.id,
+          'created_at', e.created_at,
+          'comments', coalesce((
+            select json_agg(json_build_object(
+              'content', c.content,
+              'created_at', c.created_at,
+              'id', c.id,
+              'profile', (
+                select json_build_object(
+                  'first_name', case when sc.profile_id is not null then (select anonymize_name(p.first_name, 'first')) else p.first_name end,
+                  'id', p.id,
+                  'image_uri', case when sc.profile_id is not null then null else p.image_uri end,
+                  'last_name', case when sc.profile_id is not null then (select anonymize_name(p.last_name, 'last')) else p.last_name end
+                )
+                from profiles p
+                left join subject_clients sc on p.id = sc.profile_id and e.subject_id = sc.subject_id
+                where p.id = c.profile_id
+              )
+            ) order by c.created_at asc)
+            from comments c
+            left join profiles p on c.profile_id = p.id
+            where c.event_id = e.id
+          ), '[]'::json),
+          'inputs', coalesce((
+            select json_agg(json_build_object(
+              'input', json_build_object(
+                'id', i.id,
+                'label', i.label,
+                'type', i.type
+              ),
+              'option', json_build_object(
+                'id', io.id,
+                'label', io.label
+              ),
+              'value', ei.value
+            ))
+            from event_inputs ei
+            left join inputs i on ei.input_id = i.id
+            left join input_options io on ei.input_option_id = io.id
+            where ei.event_id = e.id
+          ), '[]'::json),
+          'profile', (
+            select json_build_object(
+              'first_name', case when sc.profile_id is not null then
+              (select anonymize_name(p.first_name, 'first')) else p.first_name end,
+              'id', p.id,
+              'image_uri', case when sc.profile_id is not null then null else p.image_uri end,
+              'last_name', case when sc.profile_id is not null then (select anonymize_name(p.last_name, 'last')) else p.last_name end
+            )
+            from profiles p
+            left join subject_clients sc on p.id = sc.profile_id and e.subject_id = sc.subject_id
+            where p.id = e.profile_id
+          ),
+          'type', (select json_build_object(
+            'id', et.id,
+            'name', et.name,
+            'order', et.order,
+            'session', (select json_build_object(
+              'id', s.id,
+              'order', s.order,
+              'protocol', (select json_build_object(
+                'id', p.id,
+                'name', p.name
+              )
+              from protocols p
+              where p.id = s.protocol_id),
+              'title', s.title
+            )
+            from sessions s
+            where s.id = et.session_id)
+          )
+          from event_types et
+          where et.id = e.event_type_id
+        )
+      ) as event_info, e.created_at
+      from events e
+      join subjects s on e.subject_id = s.id
+      where e.subject_id = public_subject_id and s.public = true
+        and (start_date IS NULL OR e.created_at >= start_date)
+        and (end_date IS NULL OR e.created_at < end_date)
+      order by e.created_at desc
+      offset from_arg
+      limit limit_count
+    ) as sub;
+    return result;
+  end;
+  $$;
+
+create or replace function public.get_public_event(public_event_id uuid)
+  returns json
+  language plpgsql
+  security definer
+  as $$
+  declare
+    result json;
+  begin
+    select
+      json_build_object(
+        'comments', coalesce((
+          select json_agg(json_build_object(
+            'content', c.content,
+            'created_at', c.created_at,
+            'id', c.id,
+            'profile', (
+              select json_build_object(
+                'first_name', case when sc.profile_id is not null then (select anonymize_name(p.first_name, 'first')) else p.first_name end,
+                'id', p.id,
+                'image_uri', case when sc.profile_id is not null then null else p.image_uri end,
+                'last_name', case when sc.profile_id is not null then (select anonymize_name(p.last_name, 'last')) else p.last_name end
+              )
+              from profiles p
+              left join subject_clients sc on p.id = sc.profile_id and e.subject_id = sc.subject_id
+              where p.id = c.profile_id
+            )
+          ) order by c.created_at asc)
+          from comments c
+          join profiles p on c.profile_id = p.id
+          where c.event_id = e.id
+        ), '[]'::json),
+        'created_at', e.created_at,
+        'id', e.id,
+        'inputs', coalesce((
+          select json_agg(json_build_object(
+            'id', ei.id,
+            'input_id', ei.input_id,
+            'input_option_id', ei.input_option_id,
+            'value', ei.value
+          ))
+          from event_inputs ei
+          where ei.event_id = e.id
+        ), '[]'::json),
+        'profile', (
+          select json_build_object(
+            'first_name', case when sc.profile_id is not null then (select anonymize_name(p.first_name, 'first')) else p.first_name end,
+            'id', p.id,
+            'image_uri', case when sc.profile_id is not null then null else p.image_uri end,
+            'last_name', case when sc.profile_id is not null then (select anonymize_name(p.last_name, 'last')) else p.last_name end
+          )
+          from profiles p
+          left join subject_clients sc on p.id = sc.profile_id and e.subject_id = sc.subject_id
+          where p.id = e.profile_id
+        ),
+        'type', (
+          select json_build_object(
+            'content', et.content,
+            'id', et.id,
+            'inputs', coalesce((
+              select json_agg(json_build_object(
+                'input', (
+                  select json_build_object(
+                    'id', i.id,
+                    'label', i.label,
+                    'options', (
+                      select json_agg(json_build_object(
+                        'id', io.id,
+                        'label', io.label
+                      ))
+                      from input_options io
+                      where io.input_id = i.id
+                    ),
+                    'settings', i.settings,
+                    'type', i.type
+                  )
+                  from inputs i
+                  where i.id = eti.input_id
+                )
+              ))
+              from event_type_inputs eti
+              where eti.event_type_id = et.id
+            ), '[]'::json),
+            'name', et.name,
+            'order', et.order,
+            'session', (
+              select json_build_object(
+                'id', s.id,
+                'protocol', (
+                  select json_build_object(
+                    'id', p.id,
+                    'name', p.name
+                  )
+                  from protocols p
+                  where p.id = s.protocol_id
+                ),
+                'order', s.order
+              )
+              from sessions s
+              where s.id = et.session_id
+            )
+          )
+          from event_types et
+          where et.id = e.event_type_id
+        )
+      )
+      into result
+      from events e
+      join subjects s on e.subject_id = s.id
+      where e.id = public_event_id and s.public = true;
+    return result;
+  end;
+  $$;
+
+create or replace function public.get_public_session_with_details(public_session_id uuid)
+  returns json
+  language plpgsql
+  security definer
+  as $$
+  declare
+    result json;
+  begin
+    select json_build_object(
+      'id', s.id,
+      'order', s.order,
+      'scheduled_for', s.scheduled_for,
+      'title', s.title,
+      'modules', coalesce((
+        select json_agg(json_build_object(
+          'content', et.content,
+          'event', (
+            select json_agg(json_build_object(
+              'comments', coalesce((
+                select json_agg(json_build_object(
+                  'content', c.content,
+                  'created_at', c.created_at,
+                  'id', c.id,
+                  'profile', (
+                    select json_build_object(
+                      'first_name', case when sc.profile_id is not null then (select anonymize_name(p.first_name, 'first')) else p.first_name end,
+                      'id', p.id,
+                      'image_uri', case when sc.profile_id is not null then null else p.image_uri end,
+                      'last_name', case when sc.profile_id is not null then (select anonymize_name(p.last_name, 'last')) else p.last_name end
+                    )
+                    from profiles p
+                    left join subject_clients sc on p.id = sc.profile_id and e.subject_id = sc.subject_id
+                    where p.id = c.profile_id
+                  )
+                ))
+                from comments c
+                join profiles p on c.profile_id = p.id
+                where c.event_id = e.id
+              ), '[]'::json),
+              'created_at', e.created_at,
+              'id', e.id,
+              'inputs', coalesce((
+                select json_agg(json_build_object(
+                  'id', ei.id,
+                  'input_id', ei.input_id,
+                  'input_option_id', ei.input_option_id,
+                  'value', ei.value
+                ))
+                from event_inputs ei
+                where ei.event_id = e.id
+              ), '[]'::json),
+              'profile', (
+                select json_build_object(
+                  'first_name', case when sc.profile_id is not null then (select anonymize_name(p.first_name, 'first')) else p.first_name end,
+                  'id', p.id,
+                  'image_uri', case when sc.profile_id is not null then null else p.image_uri end,
+                  'last_name', case when sc.profile_id is not null then (select anonymize_name(p.last_name, 'last')) else p.last_name end
+                )
+                from profiles p
+                left join subject_clients sc on p.id = sc.profile_id and e.subject_id = sc.subject_id
+                where p.id = e.profile_id
+              )
+            ))
+            from events e
+            where e.event_type_id = et.id
+          ),
+          'id', et.id,
+          'inputs', coalesce((
+            select json_agg(json_build_object(
+              'input', (
+                select json_build_object(
+                  'id', i.id,
+                  'label', i.label,
+                  'options', coalesce((
+                    select json_agg(json_build_object(
+                      'id', io.id,
+                      'label', io.label
+                    ))
+                    from input_options io
+                    where io.input_id = i.id
+                  ), '[]'::json),
+                  'settings', i.settings,
+                  'type', i.type
+                )
+                from inputs i
+                where i.id = eti.input_id
+              )
+            ))
+            from event_type_inputs eti
+            where eti.event_type_id = et.id
+          ), '[]'::json),
+          'name', et.name,
+          'order', et.order
+        ) order by et.order)
+        from event_types et
+        where et.archived = false and et.session_id = s.id
+      ), '[]'::json)
+    )
+    into result
+    from sessions s
+    join protocols p on s.protocol_id = p.id
+    join subjects sub on p.subject_id = sub.id
+    where s.id = public_session_id and sub.public = true;
+    return result;
+  end;
+  $$;
+
+-- update data
+
+insert into storage.buckets (id, name, public)
+  values ('teams', 'teams', true);
+
+update auth.users
+  set raw_app_meta_data = jsonb_set(
+    raw_app_meta_data,
+    '{active_team_id}',
+    to_jsonb(users.id),
+    true
+  );
+
+update auth.users
+  set raw_app_meta_data = jsonb_set(
+    raw_app_meta_data,
+    '{is_client}',
+    to_jsonb(coalesce(users.raw_user_meta_data ->> 'is_client', 'false')::boolean),
+    true
+  );
+
+update auth.users
+  set raw_user_meta_data = raw_user_meta_data - 'is_client';
+
+-- drop old stuff
+
+drop trigger if exists on_insert_team on public.teams;
+
+drop function public.handle_insert_team ();
+drop function public.join_subject_as_manager (share_code text);
+
+alter table "public"."subject_managers" drop constraint "subject_managers_profile_id_fkey";
+alter table "public"."subject_managers" drop constraint "subject_managers_subject_id_fkey";
+alter table "public"."subject_managers" drop constraint "subject_managers_pkey";
+alter table "public"."teams" drop column "owner";
+
+drop index if exists "public"."subject_managers_pkey";
+drop index if exists "public"."subject_managers_subject_id_index";
+
+drop table "public"."subject_managers";
+
+-- add new rls policies
+
+create or replace function public.check_tag_roles(
+    _allow_if_role_is_one_of public.team_member_role[],
+    _tag_ids uuid[],
+    _team_id uuid,
+    _user_id uuid
+  )
+  returns boolean
+  language plpgsql
+  as $$
+  declare
+    matched_tag_roles public.team_member_role[] := '{}';
+  begin
+    if array_length(_tag_ids, 1) > 0 then
+      select coalesce(array_agg(tm.role), '{}') into matched_tag_roles
+        from public.team_member_tag_roles tm
+        where tm.profile_id = _user_id
+        and tm.team_id = _team_id
+        and tm.tag_id = any(_tag_ids);
+      if array_length(array(select unnest(matched_tag_roles) intersect select unnest(_allow_if_role_is_one_of)), 1) > 0 then
+        return true;
+      end if;
+    end if;
+    return false;
+  end;
+  $$;
+
+create or replace function public.check_subject_roles(
+    _allow_if_role_is_one_of public.team_member_role[],
+    _subject_id uuid,
+    _team_id uuid,
+    _user_id uuid
+  )
+  returns boolean
+  language plpgsql
+  as $$
+  begin
+    if (
+
+       (select tm.role from public.team_member_subject_roles tm
+         where tm.team_id = _team_id
+           and tm.profile_id = _user_id
+           and tm.subject_id = _subject_id
+         limit 1)
+       = any(_allow_if_role_is_one_of)
+    ) then
+      return true;
+    end if;
+    return false;
+  end;
+  $$;
+
+create or replace function public.authorize(
+    allow_if_is_subject_client boolean default false,
+    allow_if_profile_id_is_user_id boolean default false,
+    allow_if_role_is_one_of public.team_member_role[] default null,
+    allow_if_team_has_no_owner boolean default false,
+    object_input_id uuid default null,
+    object_profile_id uuid default null,
+    object_subject_id uuid default null,
+    object_team_id uuid default null,
+    object_template_id uuid default null
+  )
+  returns boolean
+  language plpgsql
+  security definer
+  set search_path = ''
+  as $$
+  declare
+    object_tag_ids uuid[];
+    team_has_owner boolean;
+    user_id uuid;
+  begin
+    select auth.uid()::uuid into user_id;
+
+    -- Check if profile ID matches user ID
+    if allow_if_profile_id_is_user_id and object_profile_id is not null and user_id = object_profile_id then
+      return true;
+    end if;
+
+    -- Check if the user is a subject client
+    if allow_if_is_subject_client and object_subject_id is not null and exists (
+      select 1 from public.subject_clients sc where sc.subject_id = object_subject_id and sc.profile_id = user_id
+    ) then
+      return true;
+    end if;
+
+    -- Resolve the team ID if necessary
+    if allow_if_role_is_one_of is not null and array_length(allow_if_role_is_one_of, 1) > 0 then
+      if object_team_id is null then
+        if object_subject_id is not null then
+          select s.team_id into object_team_id
+            from public.subjects s
+            where s.id = object_subject_id;
+        end if;
+        if object_input_id is not null then
+          select i.team_id into object_team_id
+            from public.inputs i
+            where i.id = object_input_id;
+        end if;
+        if object_template_id is not null then
+          select t.team_id into object_team_id
+            from public.templates t
+            where t.id = object_template_id;
+        end if;
+      end if;
+
+      -- Check if the user has one of the allowed roles in the team
+      if object_team_id is not null then
+        if (
+          select tm.role from public.team_members tm
+            where tm.team_id = object_team_id
+            and tm.profile_id = user_id
+            limit 1
+        ) = any(allow_if_role_is_one_of) then
+          return true;
+        end if;
+
+        -- Additional checks for subjects and tags
+        if object_subject_id is not null then
+          if public.check_subject_roles(
+            _allow_if_role_is_one_of => allow_if_role_is_one_of,
+            _subject_id => object_subject_id,
+            _team_id => object_team_id,
+            _user_id => user_id
+          ) then
+            return true;
+          end if;
+          select array_agg(tm.tag_id) into object_tag_ids
+            from public.subject_tags tm
+            where tm.subject_id = object_subject_id;
+          if public.check_tag_roles(
+            _allow_if_role_is_one_of => allow_if_role_is_one_of,
+            _tag_ids => object_tag_ids,
+            _team_id => object_team_id,
+            _user_id => user_id
+          ) then
+            return true;
+          end if;
+        end if;
+
+        -- Additional checks for inputs and tags
+        if object_input_id is not null then
+          select array_agg(it.tag_id) into object_tag_ids
+            from public.input_tags it
+            where it.input_id = object_input_id;
+          if public.check_tag_roles(
+            _allow_if_role_is_one_of => allow_if_role_is_one_of,
+            _tag_ids => object_tag_ids,
+            _team_id => object_team_id,
+            _user_id => user_id
+          ) then
+            return true;
+          end if;
+        end if;
+
+        -- Additional checks for templates and tags
+        if object_template_id is not null then
+          select array_agg(it.tag_id) into object_tag_ids
+            from public.template_tags it
+            where it.template_id = object_template_id;
+          if public.check_tag_roles(
+            _allow_if_role_is_one_of => allow_if_role_is_one_of,
+            _tag_ids => object_tag_ids,
+            _team_id => object_team_id,
+            _user_id => user_id
+          ) then
+            return true;
+          end if;
+        end if;
+      end if;
+    end if;
+
+    -- Check if the team has no owner, if allowed
+    if allow_if_team_has_no_owner and object_team_id is not null then
+      select exists (
+        select 1 from public.team_members tm
+        where tm.team_id = object_team_id
+        and tm.role = 'owner'
+      ) into team_has_owner;
+
+      if not team_has_owner then
+        return true;
+      end if;
+    end if;
+
+    -- Handle inputs linked to event types
+    if object_input_id is not null then
+      for object_subject_id in
+        select distinct et.subject_id
+          from public.event_type_inputs eti
+          join public.event_types et on et.id = eti.event_type_id
+          where eti.input_id = object_input_id
+      loop
+        if public.authorize(
+          allow_if_is_subject_client => allow_if_is_subject_client,
+          allow_if_role_is_one_of => allow_if_role_is_one_of,
+          object_subject_id => object_subject_id
+        ) then
+          return true;
+        end if;
+      end loop;
+    end if;
+
+    return false;
+  end;
+  $$;
+
+create or replace function public.get_subject_id_from_event_id(event_id uuid)
+  returns uuid
+  language plpgsql
+  as $$
+  begin
+    return (select event_types.subject_id
+      from events
+      join event_types on events.event_type_id = event_types.id
+      where events.id = event_id);
+  end;
+  $$;
+
+create or replace function public.get_subject_id_from_event_type_id(event_type_id uuid)
+  returns uuid
+  language plpgsql
+  as $$
+  begin
+    return (select event_types.subject_id
+      from event_types
+      where event_types.id = event_type_id);
+  end;
+  $$;
+
+create or replace function public.get_subject_id_from_protocol_id(protocol_id uuid)
+  returns uuid
+  language plpgsql
+  as $$
+  begin
+    return (select protocols.subject_id
+      from protocols
+      where protocols.id = protocol_id);
+  end;
+  $$;
+
+create policy "select_policy" on "public"."comments" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_id(event_id)))));
+
+create policy "insert_policy" on "public"."comments" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_id(event_id)))));
+
+create policy "delete_policy" on "public"."comments" for delete to authenticated using ((select public.authorize(
+  allow_if_profile_id_is_user_id => true,
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_profile_id => profile_id,
+  object_subject_id => (select public.get_subject_id_from_event_id(event_id)))));
+
+create policy "select_policy" on "public"."event_inputs" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_id(event_id)))));
+
+create policy "insert_policy" on "public"."event_inputs" for insert to authenticated with check ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_id(event_id)))));
+
+create policy "update_policy" on "public"."event_inputs" for update to authenticated
+  using ((select public.authorize(
+    allow_if_is_subject_client => true,
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+    object_subject_id => (select public.get_subject_id_from_event_id(event_id)))))
+  with check ((select public.authorize(
+    allow_if_is_subject_client => true,
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+    object_subject_id => (select public.get_subject_id_from_event_id(event_id)))));
+
+create policy "delete_policy" on "public"."event_inputs" for delete to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_id(event_id)))));
+
+create policy "select_policy" on "public"."event_type_inputs" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_type_id(event_type_id)))));
+
+create policy "insert_policy" on "public"."event_type_inputs" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_type_id(event_type_id)))));
+
+create policy "delete_policy" on "public"."event_type_inputs" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_event_type_id(event_type_id)))));
+
+create policy "select_policy" on "public"."event_types" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "insert_policy" on "public"."event_types" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "update_policy" on "public"."event_types" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)));
+
+create policy "delete_policy" on "public"."event_types" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "select_policy" on "public"."events" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "insert_policy" on "public"."events" for insert to authenticated with check ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "update_policy" on "public"."events" for update to authenticated
+  using ((select public.authorize(
+    allow_if_is_subject_client => true,
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+    object_subject_id => subject_id)))
+  with check ((select public.authorize(
+    allow_if_is_subject_client => true,
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+    object_subject_id => subject_id)));
+
+create policy "delete_policy" on "public"."events" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "select_policy" on "public"."input_options" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_input_id => input_id)));
+
+create policy "insert_policy" on "public"."input_options" for insert to authenticated with check ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+  object_input_id => input_id)));
+
+create policy "update_policy" on "public"."input_options" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_input_id => input_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_input_id => input_id)));
+
+create policy "delete_policy" on "public"."input_options" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_input_id => input_id)));
+
+create policy "select_policy" on "public"."input_subjects" for select to authenticated using (true);
+
+create policy "insert_policy" on "public"."input_subjects" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_input_id => input_id)));
+
+create policy "delete_policy" on "public"."input_subjects" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_input_id => input_id)));
+
+create policy "select_policy" on "public"."input_tags" for select to authenticated using (true);
+
+create policy "insert_policy" on "public"."input_tags" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_input_id => input_id)));
+
+create policy "update_policy" on "public"."input_tags" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_input_id => input_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_input_id => input_id)));
+
+create policy "delete_policy" on "public"."input_tags" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_input_id => input_id)));
+
+create policy "select_policy" on "public"."inputs" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_input_id => id,
+  object_team_id => team_id)));
+
+create policy "insert_policy" on "public"."inputs" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "update_policy" on "public"."inputs" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)));
+
+create policy "delete_policy" on "public"."inputs" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "select_policy" on "public"."insights" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "insert_policy" on "public"."insights" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "update_policy" on "public"."insights" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)));
+
+create policy "delete_policy" on "public"."insights" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "select_policy" on "public"."notifications" for select to authenticated using ((select public.authorize(
+  allow_if_profile_id_is_user_id => true,
+  object_profile_id => profile_id)));
+
+create policy "delete_policy" on "public"."notifications" for delete to authenticated using ((select public.authorize(
+  allow_if_profile_id_is_user_id => true,
+  object_profile_id => profile_id)));
+
+create policy "select_policy" on "public"."profiles" for select to authenticated using (true);
+
+create policy "select_policy" on "public"."protocols" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "insert_policy" on "public"."protocols" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "update_policy" on "public"."protocols" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)));
+
+create policy "delete_policy" on "public"."protocols" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "select_policy" on "public"."sessions" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_protocol_id(protocol_id)))));
+
+create policy "insert_policy" on "public"."sessions" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_protocol_id(protocol_id)))));
+
+create policy "update_policy" on "public"."sessions" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => (select public.get_subject_id_from_protocol_id(protocol_id)))))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => (select public.get_subject_id_from_protocol_id(protocol_id)))));
+
+create policy "delete_policy" on "public"."sessions" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => (select public.get_subject_id_from_protocol_id(protocol_id)))));
+
+create policy "select_policy" on "public"."subject_clients" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "delete_policy" on "public"."subject_clients" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "select_policy" on "public"."subject_notes" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => id)));
+
+create policy "insert_policy" on "public"."subject_notes" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+  object_subject_id => id)));
+
+create policy "update_policy" on "public"."subject_notes" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+    object_subject_id => id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder']::public.team_member_role[],
+    object_subject_id => id)));
+
+create policy "select_policy" on "public"."subject_tags" for select to authenticated using (true);
+
+create policy "insert_policy" on "public"."subject_tags" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "update_policy" on "public"."subject_tags" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => subject_id)));
+
+create policy "delete_policy" on "public"."subject_tags" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_subject_id => subject_id)));
+
+create policy "select_policy" on "public"."subjects" for select to authenticated using ((select public.authorize(
+  allow_if_is_subject_client => true,
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  object_subject_id => id,
+  object_team_id => team_id)));
+
+create policy "insert_policy" on "public"."subjects" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "update_policy" on "public"."subjects" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)));
+
+create policy "select_policy" on "public"."tags" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "insert_policy" on "public"."tags" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "update_policy" on "public"."tags" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)));
+
+create policy "delete_policy" on "public"."tags" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "select_policy" on "public"."team_member_subject_roles" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "insert_policy" on "public"."team_member_subject_roles" for insert to authenticated with check (
+  role != 'owner'::public.team_member_role and
+  (select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)));
+
+create policy "update_policy" on "public"."team_member_subject_roles" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)))
+  with check (
+    role != 'owner'::public.team_member_role and
+    (select public.authorize(
+      allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+      object_team_id => team_id)));
+
+create policy "delete_policy" on "public"."team_member_subject_roles" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "select_policy" on "public"."team_member_tag_roles" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "insert_policy" on "public"."team_member_tag_roles" for insert to authenticated with check (
+  role != 'owner'::public.team_member_role and
+  (select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)));
+
+create policy "update_policy" on "public"."team_member_tag_roles" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)))
+  with check (
+    role != 'owner'::public.team_member_role and
+    (select public.authorize(
+      allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+      object_team_id => team_id)));
+
+create policy "delete_policy" on "public"."team_member_tag_roles" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "select_policy" on "public"."team_members" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "insert_policy" on "public"."team_members" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of =>
+    case
+      when role = 'owner' then array['owner']::public.team_member_role[]
+      else array['owner', 'admin']::public.team_member_role[]
+    end,
+  allow_if_team_has_no_owner => true,
+  object_team_id => team_id)));
+
+create policy "update_policy" on "public"."team_members" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of =>
+      case
+        when role = 'owner' then array['owner']::public.team_member_role[]
+        else array['owner', 'admin']::public.team_member_role[]
+      end,
+    object_team_id => team_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of =>
+      case
+        when role = 'owner' then array['owner']::public.team_member_role[]
+        else array['owner', 'admin']::public.team_member_role[]
+      end,
+    object_team_id => team_id)));
+
+create policy "delete_policy" on "public"."team_members" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "select_policy" on "public"."subscriptions" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "select_policy" on "public"."teams" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+  allow_if_team_has_no_owner => true,
+  object_team_id => id)));
+
+create policy "insert_policy" on "public"."teams" for insert to authenticated with check (true);
+
+create policy "update_policy" on "public"."teams" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => id)));
+
+create policy "select_policy" on "public"."template_subjects" for select to authenticated using (true);
+
+create policy "insert_policy" on "public"."template_subjects" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_template_id => template_id)));
+
+create policy "delete_policy" on "public"."template_subjects" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_template_id => template_id)));
+
+create policy "select_policy" on "public"."template_tags" for select to authenticated using (true);
+
+create policy "insert_policy" on "public"."template_tags" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_template_id => template_id)));
+
+create policy "update_policy" on "public"."template_tags" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_template_id => template_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_template_id => template_id)));
+
+create policy "delete_policy" on "public"."template_tags" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_template_id => template_id)));
+
+create policy "select_policy" on "public"."templates" for select to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "insert_policy" on "public"."templates" for insert to authenticated with check ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "update_policy" on "public"."templates" for update to authenticated
+  using ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)))
+  with check ((select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_team_id => team_id)));
+
+create policy "delete_policy" on "public"."templates" for delete to authenticated using ((select public.authorize(
+  allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+  object_team_id => team_id)));
+
+create policy "subjects_select_policy" on "storage"."objects" as permissive for select to authenticated using (
+  (bucket_id = 'subjects'::text) and
+  (select public.authorize(
+    allow_if_is_subject_client => true,
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+    object_subject_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "subjects_insert_policy" on "storage"."objects" as permissive for insert to authenticated with check (
+  (bucket_id = 'subjects'::text) and
+  (select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "subjects_update_policy" on "storage"."objects" as permissive for update to authenticated
+  using (
+    (bucket_id = 'subjects'::text) and
+    (select public.authorize(
+      allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+      object_subject_id => ((storage.foldername (objects.name))[1])::uuid)))
+  with check (
+    (bucket_id = 'subjects'::text) and
+    (select public.authorize(
+      allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+      object_subject_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "subjects_delete_policy" on "storage"."objects" as permissive for delete to authenticated using (
+  (bucket_id = 'subjects'::text) and
+  (select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin']::public.team_member_role[],
+    object_subject_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "profiles_select_policy" on "storage"."objects" as permissive for select to authenticated using
+  (bucket_id = 'profiles'::text);
+
+create policy "profiles_insert_policy" on "storage"."objects" as permissive for insert to authenticated with check (
+  (bucket_id = 'profiles'::text) and
+  (select public.authorize(
+    allow_if_profile_id_is_user_id => true,
+    object_profile_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "profiles_update_policy" on "storage"."objects" as permissive for update to authenticated
+  using (
+    (bucket_id = 'profiles'::text) and
+    (select public.authorize(
+      allow_if_profile_id_is_user_id => true,
+      object_profile_id => ((storage.foldername (objects.name))[1])::uuid)))
+  with check (
+    (bucket_id = 'profiles'::text) and
+    (select public.authorize(
+      allow_if_profile_id_is_user_id => true,
+      object_profile_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "profiles_delete_policy" on "storage"."objects" as permissive for delete to authenticated using (
+  (bucket_id = 'profiles'::text) and
+  (select public.authorize(
+    allow_if_profile_id_is_user_id => true,
+    object_profile_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "teams_select_policy" on "storage"."objects" as permissive for select to authenticated using (
+  (bucket_id = 'teams'::text) and
+  (select public.authorize(
+    allow_if_role_is_one_of => array['owner', 'admin', 'recorder', 'viewer']::public.team_member_role[],
+    object_team_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "teams_insert_policy" on "storage"."objects" as permissive for insert to authenticated with check (
+  (bucket_id = 'teams'::text) and
+  (select public.authorize(
+    allow_if_role_is_one_of => array['owner']::public.team_member_role[],
+    object_team_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "teams_update_policy" on "storage"."objects" as permissive for update to authenticated
+  using (
+    (bucket_id = 'teams'::text) and
+    (select public.authorize(
+      allow_if_role_is_one_of => array['owner']::public.team_member_role[],
+      object_team_id => ((storage.foldername (objects.name))[1])::uuid)))
+  with check (
+    (bucket_id = 'teams'::text) and
+    (select public.authorize(
+      allow_if_role_is_one_of => array['owner']::public.team_member_role[],
+      object_team_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+create policy "teams_delete_policy" on "storage"."objects" as permissive for delete to authenticated using (
+  (bucket_id = 'teams'::text) and
+  (select public.authorize(
+    allow_if_role_is_one_of => array['owner']::public.team_member_role[],
+    object_team_id => ((storage.foldername (objects.name))[1])::uuid)));
+
+-- update indexes
+
+alter table "public"."subject_clients" drop constraint "subject_clients_pkey";
+drop index if exists "public"."subject_clients_subject_id_index";
+drop index if exists "public"."team_members_team_id_index";
+drop index if exists "public"."subject_clients_pkey";
+create unique index subject_clients_pkey on public.subject_clients using btree (subject_id, profile_id);
+alter table "public"."subject_clients" add constraint "subject_clients_pkey" primary key using index "subject_clients_pkey";
+create index notifications_profile_id_created_at_index on public.notifications using btree (profile_id, created_at);
diff --git a/tailwind.config.js b/tailwind.config.js
index f81fb298..06736756 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -11,25 +11,27 @@ module.exports = {
       none: '0',
       sm: 'var(--radius-sm)',
     },
-    colors: {
-      'accent-1': 'var(--color-accent-1)',
-      'accent-2': 'var(--color-accent-2)',
-      'alpha-0': 'var(--color-alpha-0)',
-      'alpha-1': 'var(--color-alpha-1)',
-      'alpha-2': 'var(--color-alpha-2)',
-      'alpha-3': 'var(--color-alpha-3)',
-      'alpha-4': 'var(--color-alpha-4)',
-      'alpha-reverse-1': 'var(--color-alpha-reverse-1)',
-      'alpha-reverse-2': 'var(--color-alpha-reverse-2)',
-      'bg-1': 'var(--color-bg-1)',
-      'bg-2': 'var(--color-bg-2)',
-      'bg-3': 'var(--color-bg-3)',
-      'fg-1': 'var(--color-fg-1)',
-      'fg-2': 'var(--color-fg-2)',
-      'fg-3': 'var(--color-fg-3)',
-      'fg-4': 'var(--color-fg-4)',
-      'red-1': 'var(--color-red-1)',
-      transparent: 'transparent',
+    extend: {
+      colors: {
+        'accent-1': 'var(--color-accent-1)',
+        'accent-2': 'var(--color-accent-2)',
+        'alpha-0': 'var(--color-alpha-0)',
+        'alpha-1': 'var(--color-alpha-1)',
+        'alpha-2': 'var(--color-alpha-2)',
+        'alpha-3': 'var(--color-alpha-3)',
+        'alpha-4': 'var(--color-alpha-4)',
+        'alpha-reverse-1': 'var(--color-alpha-reverse-1)',
+        'alpha-reverse-2': 'var(--color-alpha-reverse-2)',
+        'bg-1': 'var(--color-bg-1)',
+        'bg-2': 'var(--color-bg-2)',
+        'bg-3': 'var(--color-bg-3)',
+        'fg-1': 'var(--color-fg-1)',
+        'fg-2': 'var(--color-fg-2)',
+        'fg-3': 'var(--color-fg-3)',
+        'fg-4': 'var(--color-fg-4)',
+        'red-1': 'var(--color-red-1)',
+        transparent: 'transparent',
+      },
     },
     fontFamily: {
       body: 'var(--font-body)',
diff --git a/tailwind.css b/tailwind.css
index 2e154db5..1fcdb2d4 100644
--- a/tailwind.css
+++ b/tailwind.css
@@ -43,7 +43,7 @@
 
   /* hack to left align ios date inputs */
   input::-webkit-date-and-time-value {
-    text-align: left;
+    @apply text-left;
   }
 
   /* hack to style observable plot titles */
@@ -61,6 +61,7 @@
     @apply fill-bg-1 stroke-0 text-fg-1;
   }
 
+  /* hack to style observable plot swatches */
   figure[class^='plot-'] div[class$='-swatches-wrap'] {
     @apply gap-1 px-4 pt-4;
   }