ai-chat plugin adds a working AI chat to your app: multi-conversation history, per-user ownership, and Google Gemini responses through the Vercel AI SDK. Like every Ship plugin, it merges into your codebase — two resources on the API, a route tree on the web, and a tiny @ship/ai package you own and can edit.
Requirements
- PostgreSQL + TanStack Start (full-stack). Chats and messages are stored with Drizzle in PostgreSQL.
- The Auth plugin (
auth-starter). Every endpoint runs behindisAuthorized, and the web pages live inside the authenticated app shell.
Install
Pickai-chat (and auth-starter) in the plugin multiselect when you scaffold:
.env:
What merges in
Storage is PostgreSQL
Two tables, both built onbaseColumns (a uuid id plus createdAt / updatedAt / deletedAt), so they get soft-delete for free. ai_messages.chatId cascades on delete.
DbService and the migration:
The AI package
@ship/ai is a small package (packages/ai) wrapping the Vercel AI SDK. It exposes one function, generateResponse, which sends the conversation to Gemini (gemini-2.5-flash) and returns the final text. It reads GOOGLE_GENERATIVE_AI_API_KEY from the environment, and trims context to the last 50 messages.
configureAi:
Endpoints are oRPC
Every endpoint builds on the shared@/endpoint builder, runs behind isAuthorized, and declares an explicit route. Reads and writes go through the typed db.aiChats / db.aiMessages services.
send-message is where it comes together: save the user message, load the full thread, ask Gemini, persist the reply, and auto-title the chat on the first turn.
Ownership is a gate, not an if
The canEditChat middleware loads the requested chat for the current user into context.chat or throws NOT_FOUND. Because access and existence collapse into one error, an unauthorized user can’t tell whether a chat exists.
list and create only need isAuthorized; get-messages, send-message and remove stack canEditChat on top. See How Ship works for the full gate model.
Every route shows up live in the Scalar API reference at
http://localhost:3001/docs.
The web side
The chat UI is a pair of TanStack Router routes under the authenticated app shell:/app/ai-chat— start a new conversation/app/ai-chat/$chatId— an existing one
AiChatPage component. It talks to the API through the typed oRPC client and the Auth plugin’s useApiMutation hook — no hand-written fetch, fully typed end to end:
temp- placeholder), then swaps in the persisted user and assistant messages from the response. The presentational pieces — AiChatBox, AiChatMessage, AiChatInput, AiMessageSkeleton — live in the router-ignored -components/ folder, so they’re yours to restyle.
