Disabling Personal Accounts in the Next.js Supabase Turbo SaaS Kit and Only Allowing Team Accounts

Learn how to disable personal accounts in the Next.js Supabase Turbo SaaS kit and only allow team accounts

The Next.js Supabase Turbo SaaS kit is designed to allow personal accounts by default. However, you can disable the personal account view, and only allow user to access team accounts.

Let's walk through the steps to disable personal accounts in the Next.js Supabase Turbo SaaS kit:

  1. Store team slug in cookies: When a user logs in, store the team slug in a cookie. If none is provided, redirect the user to the team selection page.
  2. Set up Redirect: Redirect customers to the latest selected team account
  3. Create a Team Selection Page: Create a page where users can select the team they want to log in to.
  4. Duplicate the user settings page: Duplicate the user settings page so they can access their own settings from within the team workspace

To make sure that users are always redirected to the team selection page, you need to store the team slug in a cookie. If the team slug is not found, redirect the user to the team selection page. We will do all of this in the middleware.

First, let's create these functions in the apps/web/src/middleware.ts file:

apps/web/src/middleware.ts
const createTeamCookie = (userId: string) => `${userId}-selected-team-slug`;
function handleTeamAccountsOnly(request: NextRequest, userId: string) {
// always allow access to the teams page
if (request.nextUrl.pathname === '/home/teams') {
return NextResponse.next();
}
if (request.nextUrl.pathname === '/home') {
return redirectToTeam(request, userId);
}
if (isTeamAccountRoute(request) && !isUserRoute(request)) {
return storeTeamSlug(request, userId);
}
if (isUserRoute(request)) {
return redirectToTeam(request, userId);
}
return NextResponse.next();
}
function isUserRoute(request: NextRequest) {
const pathName = request.nextUrl.pathname;
return ['settings', 'billing', 'members'].includes(pathName.split('/')[2]!);
}
function isTeamAccountRoute(request: NextRequest) {
const pathName = request.nextUrl.pathname;
return pathName.startsWith('/home/');
}
function storeTeamSlug(request: NextRequest, userId: string): NextResponse {
const accountSlug = request.nextUrl.pathname.split('/')[2];
const response = NextResponse.next();
if (accountSlug) {
const cookieName = createTeamCookie(userId);
response.cookies.set({
name: cookieName,
value: accountSlug,
path: '/',
});
}
return response;
}
function redirectToTeam(request: NextRequest, userId: string): NextResponse {
const cookieName = createTeamCookie(userId);
const lastTeamSlug = request.cookies.get(cookieName);
if (lastTeamSlug) {
return NextResponse.redirect(
new URL(`/home/${lastTeamSlug.value}`, request.url),
);
}
return NextResponse.redirect(new URL('/home/teams', request.url));
}

We will now add the handleTeamAccountsOnly function to the middleware chain in the apps/web/src/middleware.ts file. This function will check if the user is on a team account route and store the team slug in a cookie. If the user is on the home route, it will redirect them to the team selection page.

apps/web/src/middleware.ts
{
pattern: new URLPattern({ pathname: '/home/*?' }),
handler: async (req: NextRequest, res: NextResponse) => {
const {
data: { user },
} = await getUser(req, res);
const origin = req.nextUrl.origin;
const next = req.nextUrl.pathname;
// If user is not logged in, redirect to sign in page.
if (!user) {
const signIn = pathsConfig.auth.signIn;
const redirectPath = `${signIn}?next=${next}`;
return NextResponse.redirect(new URL(redirectPath, origin).href);
}
const supabase = createMiddlewareClient(req, res);
const requiresMultiFactorAuthentication =
await checkRequiresMultiFactorAuthentication(supabase);
// If user requires multi-factor authentication, redirect to MFA page.
if (requiresMultiFactorAuthentication) {
return NextResponse.redirect(
new URL(pathsConfig.auth.verifyMfa, origin).href,
);
}
return handleTeamAccountsOnly(req, user.id);
},
}

In the above code snippet, we have added the handleTeamAccountsOnly function to the middleware chain.

Creating the Team Selection Page

Next, we need to create a team selection page where users can select the team they want to log in to. We will create a new page at apps/web/app/home/teams/page.tsx:

apps/web/app/home/teams/page.tsx
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { HomeAccountsList } from '~/home/(user)/_components/home-accounts-list';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('account:homePage');
return {
title,
};
};
function TeamsPage() {
return (
<div className={'container flex flex-col flex-1 h-screen'}>
<PageHeader
title={<Trans i18nKey={'common:routes.home'} />}
description={<Trans i18nKey={'common:homeTabDescription'} />}
/>
<PageBody>
<HomeAccountsList />
</PageBody>
</div>
);
}
export default withI18n(TeamsPage);

The page is extremely minimal, it just displays a list of teams that the user can select from. You can customize this page to fit your application's design.

Duplicating the User Settings Page

Finally, we need to duplicate the user settings page so that users can access their settings from within the team workspace.

We will create a new page called user-settings.tsx in the apps/web/app/home/[account] directory.

apps/web/app/home/[account]/user-settings/page.tsx
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { PageHeader } from '@kit/ui/page';
import UserSettingsPage, { generateMetadata } from '../../(user)/settings/page';
export { generateMetadata };
export default function Page() {
return (
<>
<PageHeader title={'User Settings'} description={<AppBreadcrumbs />} />
<UserSettingsPage />
</>
);
}

Feel free to customize the path or the content of the user settings page.

Adding the page to the Navigation Menu

Finally, you can add the Teams page to the navigation menu.

You can do this by updating the apps/web/config/team-account-navigation.config.tsx file:

apps/web/config/team-account-navigation.config.tsx
import { CreditCard, LayoutDashboard, Settings, User, Users } from 'lucide-react';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
const iconClasses = 'w-4';
const getRoutes = (account: string) => [
{
label: 'common:routes.dashboard',
path: pathsConfig.app.accountHome.replace('[account]', account),
Icon: <LayoutDashboard className={iconClasses} />,
end: true,
},
{
label: 'common:routes.settings',
collapsible: false,
children: [
{
label: 'common:routes.settings',
path: createPath(pathsConfig.app.accountSettings, account),
Icon: <Settings className={iconClasses} />,
},
{
label: 'common:routes.account',
path: createPath('/home/[account]/user-settings', account),
Icon: <User className={iconClasses} />,
},
{
label: 'common:routes.members',
path: createPath(pathsConfig.app.accountMembers, account),
Icon: <Users className={iconClasses} />,
},
featureFlagsConfig.enableTeamAccountBilling
? {
label: 'common:routes.billing',
path: createPath(pathsConfig.app.accountBilling, account),
Icon: <CreditCard className={iconClasses} />,
}
: undefined,
].filter(Boolean),
},
];
export function getTeamAccountSidebarConfig(account: string) {
return NavigationConfigSchema.parse({
routes: getRoutes(account),
style: process.env.NEXT_PUBLIC_TEAM_NAVIGATION_STYLE,
});
}
function createPath(path: string, account: string) {
return path.replace('[account]', account);
}

In the above code snippet, we have added the User Settings page to the navigation menu.

Removing Personal Account menu item

To remove the personal account menu item, you can update the apps/web/src/components/navigation.tsx file and remove the personal account menu item:

packages/features/accounts/src/components/account-selector.tsx
<CommandGroup>
<CommandItem
onSelect={() => onAccountChange(undefined)}
value={PERSONAL_ACCOUNT_SLUG}
>
<PersonalAccountAvatar />
<span className={'ml-2'}>
<Trans i18nKey={'teams:personalAccount'} />
</span>
<Icon item={PERSONAL_ACCOUNT_SLUG} />
</CommandItem>
</CommandGroup>
<CommandSeparator />

Once you remove the personal account menu item, users will only see the team accounts in the navigation menu.

Change Redirect in Layout

We now need to change the redirect (in case of errors) from /home to /home/teams. This is to avoid infinite redirects in case of errors.

apps/web/app/home/[account]/_lib/server/team-account-workspace.loader.ts
async function workspaceLoader(accountSlug: string) {
const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client);
const [workspace, user] = await Promise.all([
api.getAccountWorkspace(accountSlug),
requireUserInServerComponent(),
]);
// we cannot find any record for the selected account
// so we redirect the user to the home page
if (!workspace.data?.account) {
return redirect('/home/teams');
}
return {
...workspace.data,
user,
};
}

Conclusion

By following these steps, you can disable personal accounts in the Next.js Supabase Turbo SaaS kit and only allow team accounts.

This can help you create a more focused and collaborative environment for your users. Feel free to customize the team selection page and user settings page to fit your application's design and requirements.