Serverless Fediverse: Setting up a Minimal serverless Fediverse with Next.js and Vercel
A guide on creating a minimal serverless Fediverse instance using Next.js route.ts and deploying it on Vercel
On this page
Introduction
The Fediverse is a network of social platforms that can talk to each other through protocols such as ActivityPub. Mastodon, Pixelfed, PeerTube, and many other projects live in this space.
I had used Fediverse servers for a while, but I wanted something smaller and more personal: a minimal instance that could represent me, publish a profile, and connect my blog activity to a Fediverse identity.
I tried snac2 before, which is already lightweight compared with many full Fediverse servers. Even then, it was more than I needed for my use case. I wanted a tiny serverless experiment that I could understand end to end.
What I Wanted
For the first version, I wanted a server that could:
- Respond to basic ActivityPub and WebFinger requests.
- Expose a profile for a single user.
- Run without a traditional server.
- Stay cheap to host.
- Be easy to change while I learned the protocol.
- Connect blog publishing with a Fediverse presence.
This was not meant to replace a real multi-user Fediverse server. It was a small personal experiment.
Who This Is For
This approach is useful if you are a developer who wants to understand how the Fediverse works without running a full server.
It is not a production-ready ActivityPub implementation. A complete server needs inbox handling, outbox behavior, signatures, moderation, delivery queues, persistence, and many other details. My version focused on the smallest useful slice.
For now, I mainly use this idea to connect my site and blog posts with a Fediverse account.
The Approach
I used Next.js route handlers because they make it easy to expose small HTTP endpoints. The key pieces were:
- A WebFinger endpoint.
- Basic ActivityPub JSON responses.
- A small list of valid domains.
- Edge deployment through Vercel.
Here is a simplified WebFinger route:
// src/app/.well-known/webfinger/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { validActivitypubDomains } from '@/api-lib/fediverse';
export const runtime = 'edge';
export async function OPTIONS(req: NextRequest) {
const origin = req.nextUrl.origin || '*';
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
});
}
export async function GET(req: NextRequest) {
const resource = req.nextUrl.searchParams.get('resource');
if (!resource) {
return NextResponse.json(
{ error: 'Missing resource parameter' },
{
status: 400,
headers: { 'Content-Type': 'application/jrd+json; charset=utf-8' }
}
);
}
const match = /^acct:sivothajan@(.+)$/.exec(resource);
const domain = match?.[1]?.toLowerCase();
if (!domain || !validActivitypubDomains.includes(domain)) {
return NextResponse.json(
{ error: 'Resource not found' },
{
status: 404,
headers: { 'Content-Type': 'application/jrd+json; charset=utf-8' }
}
);
}
const jrd = {
subject: `acct:sivothajan@${domain}`,
aliases: [`https://${domain}`, `https://${domain}/fediverse`],
links: [
{
rel: 'http://webfinger.net/rel/profile-page',
type: 'text/html',
href: `https://${domain}`
},
{
rel: 'self',
type: 'application/activity+json',
href: `https://${domain}/fediverse`
},
{
rel: 'http://webfinger.net/rel/avatar',
type: 'image/png',
href: `https://${domain}/images/pfp.png`
}
]
};
return NextResponse.json(jrd, {
headers: { 'Content-Type': 'application/jrd+json; charset=utf-8' }
});
}
That route lets Fediverse software discover the actor document for a user such as:
acct:[email protected]
A Small TypeScript Note
If your route lives under .well-known, make sure TypeScript includes it. In some projects, files under that folder may not be picked up by default.
One possible fix is to update tsconfig.json:
{
"compilerOptions": {
"include": ["**/.well-known/**/*.ts", "src"]
}
}
Adjust that structure to match your own project.
Deployment
I deployed the experiment as a Next.js project on Vercel. You can see the testing account here:
The project name is Fediman.
Why I Did Not Treat It as Finished
ActivityPub is bigger than a profile endpoint and a WebFinger response. Federation becomes serious once other servers start sending requests, expecting signatures, delivering messages, and caching actor state.
So I treat this project as an experiment first. It helps me learn the protocol and connect my own site to the Fediverse, but it is not a general-purpose server yet.