AI generation persistence patterns — unique IDs, addressable URLs, database storage, and cost tracking for every LLM generation
Scanned 9/6/2026
Install to Claude Code
npx -y skills add Kodik-AI/kodik --skill ai-generation-persistence --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ai Generation Persistence?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kodik-ai-ai-generation-persistence)More formats (shields.io, HTML) on the badges page.
---
name: ai-generation-persistence
description: "AI generation persistence patterns — unique IDs, addressable URLs, database storage, and cost tracking for every LLM generation"
metadata:
priority: 6
docs:
- "https://sdk.vercel.ai/docs/ai-sdk-ui/storing-messages"
sitemap: "https://sdk.vercel.ai/sitemap.xml"
pathPatterns:
- "app/api/generate/**"
- "app/api/generations/**"
- "src/app/api/generate/**"
- "src/app/api/generations/**"
- "app/chat/[id]/**"
- "app/generate/[id]/**"
- "src/app/chat/[id]/**"
- "src/app/generate/[id]/**"
- "lib/generations/**"
- "src/lib/generations/**"
bashPatterns: []
importPatterns:
- "ai"
- "@ai-sdk/*"
- "@vercel/blob"
- "nanoid"
- "@paralleldrive/cuid2"
promptSignals:
phrases:
- "save generations"
- "persist generations"
- "generation history"
- "chat history"
- "save chat"
- "generation id"
- "ai chat"
- "chat app"
- "chatbot"
- "image generation"
- "text generation"
- "ai app"
allOf:
- [generate, save]
- [generate, persist]
- [generate, store]
- [ai, persist]
- [ai, history]
- [ai, database]
- [chat, persist]
- [chat, database]
- [chat, url]
- [generation, url]
- [generation, id]
- [image, generate]
- [stream, save]
- [stream, persist]
anyOf:
- "shareable"
- "retrievable"
- "permalink"
- "cost tracking"
- "token usage"
- "nanoid"
- "cuid"
- "openai"
- "anthropic"
- "llm"
- "gpt"
- "claude"
noneOf:
- "github actions"
- "ci workflow"
minScore: 6
---
# AI Generation Persistence
**AI generations are expensive, non-reproducible assets. Never discard them.**
Every call to an LLM costs real money and produces unique output that cannot be exactly reproduced. Treat generations like database records — assign an ID, persist immediately, and make them retrievable.
## Core Rules
1. **Generate an ID before the LLM call** — use `nanoid()` or `createId()` from `@paralleldrive/cuid2`
2. **Persist every generation** — text and metadata to database, images and files to Vercel Blob
3. **Make every generation addressable** — URL pattern: `/chat/[id]`, `/generate/[id]`, `/image/[id]`
4. **Track metadata** — model name, token usage, estimated cost, timestamp, user ID
5. **Never stream without saving** — if the user refreshes, the generation must survive
## Generate-Then-Redirect Pattern
The standard UX flow for AI features: create the resource first, then redirect to its page.
```ts
// app/api/chat/route.ts
import { nanoid } from "nanoid";
import { db } from "@/lib/db";
import { redirect } from "next/navigation";
export async function POST(req: Request) {
const { prompt, model } = await req.json();
const id = nanoid();
// Create the record BEFORE generation starts
await db.insert(generations).values({
id,
prompt,
model,
status: "pending",
createdAt: new Date(),
});
// Redirect to the generation page — it handles streaming
redirect(`/chat/${id}`);
}
```
```tsx
// app/chat/[id]/page.tsx
import { db } from "@/lib/db";
import { notFound } from "next/navigation";
export default async function ChatPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const generation = await db.query.generations.findFirst({
where: eq(generations.id, id),
});
if (!generation) notFound();
// Render with streaming if still pending, or show saved result
return <ChatView generation={generation} />;
}
```
This gives you: shareable URLs, back-button support, multi-tab sessions, and generation history for free.
## Persistence Schema
```ts
// lib/db/schema.ts
import { pgTable, text, integer, timestamp, jsonb } from "drizzle-orm/pg-core";
export const generations = pgTable("generations", {
id: text("id").primaryKey(), // nanoid
userId: text("user_id"), // auth user
model: text("model").notNull(), // "openai/gpt-5.4"
prompt: text("prompt"), // input text
result: text("result"), // generated output
imageUrls: jsonb("image_urls"), // Blob URLs for generated images
tokenUsage: jsonb("token_usage"), // { promptTokens, completionTokens }
estimatedCostCents: integer("estimated_cost_cents"),
status: text("status").default("pending"), // pending | streaming | complete | error
createdAt: timestamp("created_at").defaultNow(),
});
```
## Storage Strategy
| Data Type | Storage | Why |
|-----------|---------|-----|
| Text, metadata, history | Neon Postgres via Drizzle | Queryable, relational, supports search |
| Generated images & files | Vercel Blob (`@vercel/blob`) | Permanent URLs, CDN-backed, no expiry |
| Prompt dedup cache | Upstash Redis | Fast lookup, TTL-based expiry |
## Image Persistence
Never serve generated images as ephemeral base64 or temporary URLs. Save to Blob immediately:
```ts
import { put } from "@vercel/blob";
import { generateText } from "ai";
const result = await generateText({ model, prompt });
// Save every generated image to permanent storage
const imageUrls: string[] = [];
for (const file of result.files ?? []) {
if (file.mediaType?.startsWith("image/")) {
const ext = file.mediaType.split("/")[1] || "png";
const blob = await put(`generations/${generationId}.${ext}`, file.uint8Array, {
access: "public",
contentType: file.mediaType,
});
imageUrls.push(blob.url);
}
}
// Update the generation record with permanent URLs
await db.update(generations)
.set({ imageUrls, status: "complete" })
.where(eq(generations.id, generationId));
```
## Cost Tracking
Extract usage from every generation and store it. This enables billing, budgeting, and abuse detection:
```ts
const result = await generateText({ model, prompt });
const usage = result.usage; // { promptTokens, completionTokens, totalTokens }
const estimatedCostCents = estimateCost(model, usage);
await db.update(generations).set({
result: result.text,
tokenUsage: usage,
estimatedCostCents,
status: "complete",
}).where(eq(generations.id, generationId));
```
## Prompt Dedup / Caching
Avoid paying for identical generations. Cache by content hash:
```ts
import { Redis } from "@upstash/redis";
import { createHash } from "crypto";
const redis = Redis.fromEnv();
function hashPrompt(model: string, prompt: string): string {
return createHash("sha256").update(`${model}:${prompt}`).digest("hex");
}
// Check cache before generating
const cacheKey = `gen:${hashPrompt(model, prompt)}`;
const cached = await redis.get<string>(cacheKey);
if (cached) return cached; // Return cached generation ID
// After generation, cache the result
await redis.set(cacheKey, generationId, { ex: 3600 }); // 1hr TTL
```
## Anti-Patterns
- **Streaming to client without saving** — generation lost on page refresh. Always write to DB as tokens arrive or on completion.
- **Routes without `[id]` segments** — `/api/chat` with no ID means generations aren't addressable. Use `/chat/[id]`.
- **Re-generating identical prompts** — check cache first. Same prompt + same model = same cost for no new value.
- **Ephemeral base64 images** — generated images served inline are lost when the component unmounts. Save to Vercel Blob.
- **Missing metadata** — always store model name, token counts, and timestamp. You need this for cost tracking and debugging.
- **Client-only state** — storing generations only in React state or localStorage. Use a database — generations must survive across devices and sessions.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!