Oolvay uses Upstash as its Redis provider. Upstash is a serverless Redis service that communicates over HTTP, making it compatible with Next.js serverless functions without requiring a persistent TCP connection.
When configured, Upstash becomes the sole session store. Better Auth writes sessions to Redis instead of Postgres, and all session reads and deletions go through Redis. The Postgres session table remains empty for the lifetime of that deployment.
The Redis client in lib/redis.ts is constructed only when both UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are present. If either is missing, the client resolves to null and Better Auth falls back to Postgres automatically. There is no error thrown, no warning surfaced to users, and no degraded functionality.
The client is passed to Better Auth’s secondaryStorage interface in lib/auth/auth.ts. When secondaryStorage is active, Better Auth routes all session operations through Redis exclusively. Postgres is not involved in session reads, writes, or deletions for the lifetime of that deployment.
Create an account at console.upstash.com if you do not already have one.
Create a new Redis database.
In the Upstash console:
your-app-sessions is descriptive enough.us-east-1 or eu-west-1 depending on your deployment region. Physical distance between your application server and Redis is the primary driver of session read latency.Retrieve your credentials.
Once the database is created, open it and scroll to the Connect section. Under the REST tab (alongside TCP), Upstash displays the two values needed in a ready-to-copy .env.local block.
UPSTASH_REDIS_REST_URL: looks like 0 / 2,000 characters
https://your-database.upstash.ioUPSTASH_REDIS_REST_TOKEN: a long alphanumeric tokenAdd the credentials to your local environment.
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=Restart your development server.
bun devThe Redis client is initialized at startup. Changes to environment variables are not picked up until the server restarts.
Verify the connection.
Navigate to the infrastructure health panel in your admin dashboard. A connected Redis instance will display a latency reading on the Cache card.
| Status | Latency |
|---|---|
| Green | Below 50ms |
| Yellow | 50ms to 150ms |
| Red | Above 150ms or connection failed |
A freshly created Upstash database in the correct region should show green consistently.
Add the same two environment variables to your hosting platform.
On Vercel, go to Settings → Environment Variables, add both values, apply them to your production environment, and redeploy.
Upstash also offers a Vercel integration that injects these variables automatically. Find it in the Upstash console under Integrations.
| Limit | Value |
|---|---|
| Storage | 256 MB |
| Monthly bandwidth | 10 GB |
| Monthly commands | 500,000 |
| Databases | 1 |
For most up-to-date pricing, quotas, and plan details, refer to Upstash’s Redis pricing page.
Most early-stage applications stay comfortably within these limits. Session reads and writes are lightweight operations, and Better Auth only touches Redis on session creation, lookup, and deletion rather than on every rendered component or server action.
Oolvay uses Redis exclusively for session caching, handled entirely by Better Auth through its secondaryStorage interface. Your application code never calls Redis directly for sessions, Better Auth manages that coordination internally.
For anything beyond sessions, lib/redis.ts exposes three typed cache helpers that are ready to use without any additional setup.
import { setCache, getCache, deleteCache } from "@/lib/redis"Objects, arrays, and primitives can be stored directly. Upstash automatically serializes and deserializes values, so manual JSON.stringify() and JSON.parse() calls are unnecessary.
All three functions are null-safe. If Redis is not configured, they return false or null gracefully rather than throwing, so your application logic does not need to branch on whether Redis is available.
The ttlInSeconds parameter accepted by setCache() is optional and uses seconds as its unit. When omitted, the value is stored without an expiration.
Stores a value under a key, with an optional TTL in seconds.
await setCache("dashboard:stats:global", computedStats, 60)
// Caches the result for 60 seconds. Omit the third argument to cache indefinitely.Retrieves a value by key. Returns null if the key does not exist or Redis is unavailable.
const cached = await getCache<DashboardStats>("dashboard:stats:global")
if (cached) return cached
// Fall through to compute and store if missingRemoves a key explicitly, useful when the underlying data changes and the cached value needs to be invalidated immediately rather than waiting for TTL expiry.
await deleteCache("dashboard:stats:global")The typical pattern is to check the cache first, fall back to the source of truth if the key is missing, store the result, and return it.
import { getCache, setCache } from "@/lib/redis"
import { db } from "@/db/drizzle"
async function getAdminDashboardStats() {
const cacheKey = "admin:dashboard:stats"
const cached = await getCache<AdminStats>(cacheKey)
if (cached) return cached
const stats = await db.select(...) // expensive aggregation query
await setCache(cacheKey, stats, 120) // cache for 2 minutes
return stats
}Use colon-separated namespaces for cache keys (for example, user:tier:123, admin:dashboard:stats, or blog:post:my-post:meta) to keep keys organized and avoid collisions.
| Situation | Example cache key | Suggested TTL |
|---|---|---|
| Expensive admin aggregation queries | admin:dashboard:stats | 60 to 300 seconds |
| Resolved user tier after a subscription check | user:tier:{userId} | 60 seconds |
| Blog post metadata for frequently visited posts | blog:post:{slug}:meta | 600 seconds |
| Short-lived verification tokens outside the database | verify:email-change:{userId} | 900 seconds |
| Custom rate-limiting counters beyond Arcjet | ratelimit:api-key-regen:{userId} | 86400 seconds |
These helpers are entirely separate from how Better Auth uses Redis internally. You cannot and should not use them to read or write session data. Session keys are managed by Better Auth under its own internal key format and should only ever be touched through the Better Auth API.
Verify that both environment variables are present in your environment and that the server has been restarted since they were added. Confirm that UPSTASH_REDIS_REST_URL begins with https:// and has no trailing slash.
The database region likely does not match your deployment region. Check which region your Upstash database is in and compare it to your Vercel or hosting region. Creating a new database in a closer region and updating your environment variables is usually the fix.
Expected behavior. Removing UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN causes the Redis client to resolve to null at startup. Better Auth falls back to writing sessions to Postgres automatically. Note that any sessions that existed in Redis are not migrated. Existing Redis-backed sessions will no longer be available after the switch, so users may need to sign in again.
The most common cause is that the server restarted between the setCache() and getCache() calls during development, which reinitializes the client and does not affect Redis itself, but if UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN was missing at startup the client resolved to null and neither call actually reached Redis. Confirm both environment variables are present and restart the server.
Redis can be removed at any time by deleting the two environment variables
from your hosting platform and redeploying. Better Auth falls back to Postgres
automatically. Active user sessions stored in Redis will not carry over, so
users may need to sign in again after the switch. Application-level cache keys
will return null from getCache() until Redis is restored or the data is
recomputed.