Skip to Content
Fumadocs

Access Control

Limit the access of content.

Overview

Thanks to the flexible design, it is simple to implement access control with Fumadocs.

Loader API

If you are using loader() API, you can create the loaders on-demand, and filter files from content source to restrict accessible content.

For example, this setup with Fumadocs MDX only shows the MDX files with frontmatter permission: public.

lib/source.ts
import { docs } from 'collections/server';
import { loader, update } from 'fumadocs-core/source';

const filteredSource = update(docs.toFumadocsSource())
  .files((files) =>
    files.filter((file) => {
      // keep meta files (e.g. `meta.json`)
      if (file.type === 'meta') return true;

      // access file data (type-safe)
      return file.data.permission === 'public';
    }),
  )
  .build();

export const source = loader(filteredSource, {
  baseUrl: '/docs',
});

For more complicated setups, you can turn source into a factory function.

import { docs } from 'collections/server';
import { loader, update } from 'fumadocs-core/source';

// uncached, it's better to cache it in-memory for real use case.
export function createSource(permission: 'public' | 'admin') {
  const filteredSource = update(docs.toFumadocsSource())
    .files((files) =>
      files.filter((file) => {
        if (file.type === 'meta') return true;

        return file.data.permission === permission;
      }),
    )
    .build();

  return loader(filteredSource, {
    baseUrl: '/docs',
  });
}

Advantages

What is special with this approach is that the access is limited at input level, rather than at rendering phase or build-time.

This guarantees the protected content is filtered entirely, and without compromise on flexibility.

Custom Implementation

Without the Loader API, implement access control in React Router, such as in routes or middleware.

For example, to limit access to certain pages:

app/routes/docs.tsx
import type { Route } from './+types/docs';
import { source } from '@/lib/source';

export async function loader({ params, request }: Route.LoaderArgs) {
  const user = await getUser(request);
  const page = source.getPage(params['*'].split('/').filter((v) => v.length > 0));

  if (!page) throw new Response('Not found', { status: 404 });
  if (page.data.permission !== user.permission) throw new Response('Not found', { status: 404 });

  // return page data...
}

Note that it is up to you to manage the page tree (sidebar items), search, Markdown routes and other details.

This approach gives the most flexibility, but Fumadocs does not handle the access control. The setup takes more work, so consult the React Router docs when implementing it.

How is this guide?

Last updated on