Change your application: migrate from Upstash to KiviDB

There are two ways, and you can mix them: functions on the edge keep REST, long-running servers move to a Redis client.

There are two ways, and you can mix them: functions on the edge keep REST, long-running servers move to a Redis client.

Option A: keep @upstash/redis and use KiviDB's REST API

KiviDB Cloud's REST API speaks the same wire format as Upstash's. Turn it on under Configuration → REST API, then change the two variables your app reads and redeploy:

# Before
UPSTASH_REDIS_REST_URL="https://my-db-12345.upstash.io"
UPSTASH_REDIS_REST_TOKEN="AX..."
# After
UPSTASH_REDIS_REST_URL="https://your-name.cloud.kividb.io"
UPSTASH_REDIS_REST_TOKEN="kvr_..."

The code stays as it is:

import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();
await redis.set("user:1", { name: "Ada" }, { ex: 3600 });
const user = await redis.get("user:1");

The token is a database user's: kvr_ followed by username:password in base64url. The console shows it beside each user in Configuration → Users once the REST API is ready. A user with the Read only profile gives a read-only token, like Upstash's.

What differs from Upstash's REST API:

  • The REST API is on Essentials and Pro databases, not free or sharded ones.
  • A request can be 10 MB, a reply 64 MB, a pipeline or transaction 1,000 commands, and a request 10 seconds.
  • SELECT, SUBSCRIBE, MULTI/EXEC as separate requests, CLIENT, HELLO, AUTH, and blocking commands are refused, as on Upstash; use redis.multi() or redis.pipeline() for transactions and pipelines.
  • The port 443 endpoint follows the database's network rules: an address that can't reach the data ports can't reach it either. Functions on the edge come from many addresses, so check Configuration → Network allows them.

The REST API reference has the details.

Option B: move to a Redis client

For a server that runs continuously, a TCP client is faster than HTTP and supports blocking commands and pub/sub. One thing needs care: @upstash/redis stores objects and numbers as JSON text and parses them back on read. A regular client returns the stored text, so parse it where your code stored objects.

Node.js: @upstash/redis to ioredis

// Before
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();

await redis.set("user:1", { name: "Ada", tags: ["a", "b"] }, { ex: 3600 });
const user = await redis.get("user:1");            // { name: "Ada", tags: [...] }
await redis.hset("profile:1", { name: "Ada", age: 36 });
const profile = await redis.hgetall("profile:1");  // { name: "Ada", age: 36 }
const p = redis.pipeline();
p.incr("visits");
p.expire("visits", 86400);
await p.exec();
// After
import fs from "node:fs";
import Redis from "ioredis";

const redis = new Redis({
  host: "your-name.cloud.kividb.io",
  port: 6380,
  username: "kividbapp",
  password: process.env.KIVIDB_PASSWORD,
  tls: { ca: fs.readFileSync("kividb-ca.pem") },
});

// Values written by @upstash/redis are JSON text. Parse what parses,
// and leave plain strings as they are.
function fromUpstash(value: string | null) {
  if (value === null) return null;
  try {
    return JSON.parse(value);
  } catch {
    return value;
  }
}

await redis.set("user:1", JSON.stringify({ name: "Ada", tags: ["a", "b"] }), "EX", 3600);
const user = fromUpstash(await redis.get("user:1"));
await redis.hset("profile:1", { name: "Ada", age: 36 });
const profile = Object.fromEntries(
  Object.entries(await redis.hgetall("profile:1")).map(([k, v]) => [k, fromUpstash(v)]),
);
await redis.pipeline().incr("visits").expire("visits", 86400).exec();

Writing objects with JSON.stringify keeps them readable by any code still on @upstash/redis, so the two can run side by side during the move.

Node.js: @upstash/redis to node-redis

import fs from "node:fs";
import { createClient } from "redis";

const redis = createClient({
  url: "rediss://your-name.cloud.kividb.io:6380",
  username: "kividbapp",
  password: process.env.KIVIDB_PASSWORD,
  socket: { tls: true, ca: fs.readFileSync("kividb-ca.pem") },
});
await redis.connect();

await redis.set("user:1", JSON.stringify({ name: "Ada" }), { EX: 3600 });
const user = fromUpstash(await redis.get("user:1"));
const board = await redis.zRangeWithScores("board", 0, -1); // [{ value, score }]

Python: upstash-redis to redis-py

# Before
from upstash_redis import Redis
redis = Redis.from_env()

# After
import json, os, redis as redispy

r = redispy.Redis(
    host="your-name.cloud.kividb.io",
    port=6380,
    username="kividbapp",
    password=os.environ["KIVIDB_PASSWORD"],
    ssl=True,
    ssl_ca_certs="kividb-ca.pem",
    decode_responses=True,
)
r.set("user:1", json.dumps({"name": "Ada"}), ex=3600)
user = json.loads(r.get("user:1"))

Two more differences when you leave REST behind:

  • Connections are long-lived. Create the client once per process, not per request.
  • Numbers come back as strings. @upstash/redis turned a stored "42" into the number 42; GET on a Redis client returns the string "42". Replies to INCR and similar commands are numbers in both.