📄 SKILL.md

← Vault

name: tanstack-start-server-functions-fix

description: Fix broken /_serverFn 500 errors in TanStack Start (Lovable) projects — convert createServerFn to API route handlers when the build plugin doesn't register them in the manifest.


Fix TanStack Start /_serverFn 500 — Convert to API Routes

Symptom

All calls to POST /_serverFn return:

`json

{"status":500,"unhandled":true,"message":"HTTPError"}

`

Meanwhile GET / works, routes work, but server functions don't.

Root Cause

TanStack Start (v1.167.x used by Lovable) uses a Vite plugin (@lovable.dev/vite-tanstack-config) that runs a Babel transform during bun run build to scan route files for createServerFn calls and write them to a manifest. The server handler (dist/server/server.js) does manifest[id] lookups — if the manifest is empty, getServerFnById(id) throws and the error is caught as an HTTPError, returning 500.

The plugin only scans files that use createFileRoute (route files under src/routes/). If a subagent or developer puts createServerFn in a non-route file like src/server/gmail.functions.ts, it gets compiled away and the manifest stays empty (const manifest = {}).

Fix — Convert to API Route Handlers

Do NOT try to fix the build plugin. Replace the createServerFn system with standard TanStack Start API route handlers. This is more robust, has no build-time magic, and works 100% of the time.

Step 1 — Create the route handler file

Create the file under src/routes/api/gmail/_serverFn.ts (must be under src/routes/ to be a route):

`typescript

import { createFileRoute } from "@tanstack/react-router";

import { z } from "zod";

// Schemas

const SendSchema = z.object({

to: z.string().email(),

subject: z.string().min(1),

body: z.string().min(1),

documentIds: z.array(z.string().uuid()),

clientName: z.string(),

});

// Auth helper

async function getAuthClient(request: Request) {

const authHeader = request.headers.get("authorization");

if (!authHeader?.startsWith("Bearer ")) throw new Response("Unauthorized", { status: 401 });

const token = authHeader.replace("Bearer ", "");

// ... create supabase client with token auth

return { supabase, userId };

}

export const Route = createFileRoute("/api/gmail/_serverFn")({

server: {

handlers: {

POST: async ({ request }) => {

let body: any;

try { body = await request.json(); } catch { return Response.json({ error: { message: "Invalid JSON" } }, { status: 400 }); }

const { __fn, data } = body;

switch (__fn) {

case "getGmailConnection": { / ... / return Response.json(result); }

case "sendGmailWithAttachments": { / ... / return Response.json({ ok: true }); }

default: return Response.json({ error: { message: Unknown: ${__fn} } }, { status: 404 });

}

},

},

},

});

`

Step 2 — Update client code

Remove useServerFn imports and manual fetch("/_serverFn", ...) calls. Replace with plain fetch() to the new route:

`typescript

async function gmailFetch(fn: string, data?: unknown) {

const { data: { session } } = await supabase.auth.getSession();

if (!session) throw new Error("Não autenticado");

const res = await fetch("/api/gmail/_serverFn", {

method: "POST",

headers: {

"Content-Type": "application/json",

Authorization: Bearer ${session.access_token},

},

body: JSON.stringify({ __fn: fn, data }),

});

const json = await res.json();

if (!res.ok || json.error) throw new Error(json.error?.message ?? "Erro");

return json;

}

`

Step 3 — Delete the old file

`bash

rm src/server/gmail.functions.ts

`

Step 4 — Build and deploy

`bash

bun run build # NOT npm run build — Lovable/TanStack uses Bun

rsync dist/ to VPS

pm2 restart appname

`

Key Differences

AspectcreateServerFnAPI Route Handler
--------------------------------------------
RegistrationBuild plugin magicAutomatic (it's a route)
ManifestMust be populated by pluginNo manifest needed
Route file requiredMust use createFileRouteMust be in src/routes/
DebuggingHard — plugin is Lovable-specificEasy — standard HTTP
ExtensibilityLimitedFull Express-like control

Gotcha