Перейти к содержимому
обзор / документация

Документация

От установки CLI до кодов ошибок. Все примеры можно вставить в песочницу и запустить — раннер там настоящий.

· это рабочий сервис
Ничего не нашлось. Попробуйте kv, cron, ошибк или лимит.

Быстрый старт

Путь от пустой папки до работающего URL — четыре команды и примерно восемь секунд ожидания на последней.

терминал
$ npm i -g @tessera/cli
$ tessera login
$ tessera init my-api --template minimal
$ cd my-api && tessera deploy --prod

После деплоя CLI печатает адрес вида https://my-api.tessera.app и список точек, куда уехал бандл. Дальше правьте src/index.js и повторяйте tessera deploy — предыдущие версии остаются доступными 90 дней.

Локальная разработка tessera dev поднимает тот же рантайм на localhost:8787, включая KV и cron. Отличий от продакшена нет, кроме географии.

Структура проекта

дерево
my-api/
  src/index.js      # точка входа, экспортирует handler
  tessera.json      # конфигурация: маршруты, cron, биндинги
  .env.preview      # переменные для превью-окружения
  .tessera/         # кеш сборки, в git не нужен

Установка CLI

CLI — единственный обязательный инструмент. Нужен Node.js 18 или новее; сам рантайм на машине не требуется.

основные команды
# разработка
$ tessera dev --port 8787 --kv-persist
# деплой
$ tessera deploy            # в превью-окружение
$ tessera deploy --prod     # в продакшен, во все точки
# наблюдение
$ tessera logs --tail --pop dme1
$ tessera trace --last 20 --min-ms 40
# секреты
$ tessera secret put STRIPE_KEY --env prod
$ tessera secret ls
# версии
$ tessera versions
$ tessera rollback v41

tessera.json

tessera.json
{
  "name": "my-api",
  "main": "src/index.js",
  "compatibility": "2026-06-01",
  "routes": [
    { "pattern": "api.example.ru/*", "zone": "example.ru" }
  ],
  "kv": [{ "binding": "CACHE", "namespace": "prod-cache" }],
  "cron": [{ "expr": "*/5 * * * *", "handler": "scheduled" }],
  "limits": { "cpu_ms": 50 }
}

Первая функция

Функция — это экспортированный handler, который получает запрос и возвращает Response. Ничего наследовать и регистрировать не нужно.

export async function handler(request, env, ctx) {
  const url = new URL(request.url);

  if (url.pathname === "/health") {
    return new Response("ok", { headers: { "cache-control": "no-store" } });
  }

  return Response.json({
    pop: env.REGION,
    city: request.geo.city,
    now: new Date().toISOString()
  });
}

Третий аргумент ctx нужен для работы, которая должна продолжиться после отправки ответа:

ctx.waitUntil(env.CACHE.put(key, body, { ttl: 300 }));
Изолят не хранит состояние Глобальные переменные живут, пока живёт изолят, — от нескольких секунд до нескольких минут. Использовать их как кеш можно, как источник истины — нельзя.

Роутинг

Маршруты объявляются в tessera.json на уровне домена, а внутри функции путь разбирается как вам удобно. Готового роутера в рантайме нет — есть URLPattern.

src/index.js
const routes = [
  { m: "GET",  p: new URLPattern({ pathname: "/users/:id" }), h: getUser },
  { m: "POST", p: new URLPattern({ pathname: "/users" }),     h: createUser }
];

export async function handler(request, env, ctx) {
  for (const r of routes) {
    if (r.m !== request.method) continue;
    const match = r.p.exec(request.url);
    if (match) return r.h(request, env, match.pathname.groups);
  }
  return Response.json({ error: "not_found" }, { status: 404 });
}

Приоритет шаблонов

Если запрос подходит под несколько маршрутов из конфигурации, выигрывает более специфичный: сначала точное совпадение хоста и пути, затем шаблон с *, затем маршрут по умолчанию.

ШаблонЗапросПриоритет
api.example.ru/v2/orders/v2/orders1
api.example.ru/v2/*/v2/orders2
api.example.ru/*/v2/orders3
*.example.ru/*/v2/orders4

Запрос и ответ

request — стандартный Request плюс поле geo, которое заполняет точка присутствия.

request.geo
{
  "pop": "dme1",
  "city": "Москва",
  "country": "RU",
  "region": "MOW",
  "latitude": 55.75,
  "longitude": 37.62,
  "asn": 12389
}

Потоковый ответ

Отвечать можно до того, как готов весь payload. Соединение разрешено держать до 15 минут — этого хватает и на SSE, и на прокси к языковой модели.

SSE
export async function handler(request, env, ctx) {
  const { readable, writable } = new TransformStream();
  const writer = writable.getWriter();
  const enc = new TextEncoder();

  ctx.waitUntil((async () => {
    for (let i = 1; i <= 5; i++) {
      await writer.write(enc.encode(`data: тик ${i}\n\n`));
      await new Promise(r => setTimeout(r, 1000));
    }
    await writer.close();
  })());

  return new Response(readable, {
    headers: {
      "content-type": "text/event-stream",
      "cache-control": "no-cache",
      "x-accel-buffering": "no"
    }
  });
}
Что считается временем ответа Лимит 15 минут — на всё соединение. Лимит 50 мс — только на процессорное время. Ожидание сети и KV в CPU не входит.

Окружение и секреты

Второй аргумент env собирается из трёх источников: переменные из tessera.json, секреты из хранилища и биндинги ресурсов (KV, Blob, Counters).

терминал
$ tessera secret put STRIPE_KEY --env prod
? Значение: ********************
✔ Записано. Значение больше не покажется.
$ tessera secret ls --env prod
STRIPE_KEY   обновлён 2 дня назад
JWT_SECRET   обновлён 14 дней назад

Секрет шифруется на вашей машине и расшифровывается только внутри изолята. В логах, трейсах и дампах значение заменяется на [secret] — включая случаи, когда вы напечатали его сами.

ЧтоГде задаётсяВидно в логах
Переменнаяtessera.json → varsда
Секретtessera secret putнет, маскируется
Биндингtessera.json → kv / blobтолько имя
REGIONрантаймда

KV-хранилище

KV — ключ-значение с репликацией на все точки. Чтение из локальной реплики занимает около 0,4 мс, запись сходится по сети за две секунды. Это модель «согласованность в конечном счёте»: сразу после записи другая точка может отдать старое значение.

основные операции
// чтение
const raw = await env.CACHE.get("user:42");            // строка или null
const obj = await env.CACHE.get("user:42", "json");    // сразу разобранный JSON

// запись с временем жизни
await env.CACHE.put("user:42", JSON.stringify(user), { ttl: 3600 });

// перебор по префиксу
const { keys, cursor } = await env.CACHE.list({ prefix: "user:", limit: 100 });

// удаление
await env.CACHE.delete("user:42");

Когда KV не подходит

  • Счётчики и лимиты — берите COUNTERS: они атомарны в пределах точки.
  • Данные, где важна строгая консистентность, — держите во внешней базе и обращайтесь к ней явно.
  • Больше одной записи в секунду на один ключ — сверх лимита записи начнут отбрасываться.
Ключ — это часть API Переименовать ключ без миграции нельзя: старые изоляты продолжают жить до нескольких минут и будут читать прежнее имя. Пишите оба ключа один релиз, потом убирайте старый.

Cron и очереди

Расписание описывается в конфигурации, обработчик экспортируется рядом с основным. Задача выполняется ровно один раз: точка-лидер выбирается автоматически, остальные пропускают запуск.

src/index.js
export async function scheduled(event, env, ctx) {
  // event.cron — выражение, event.scheduledTime — плановое время
  const stale = await env.CACHE.list({ prefix: "session:" });
  for (const key of stale.keys) {
    if (key.expiresAt < Date.now()) await env.CACHE.delete(key.name);
  }
  console.log("подчищено ключей:", stale.keys.length);
}

Отладить расписание можно локально, не дожидаясь нужной минуты:

$ tessera dev --cron "*/5 * * * *"
$ tessera trigger scheduled            # ручной запуск
ВыражениеКогдаЗапусков в сутки
* * * * *каждую минуту1 440
*/5 * * * *каждые пять минут288
0 * * * *в начале каждого часа24
0 4 * * 1по понедельникам в 04:00 UTC0,14

Логи и трейсы

Всё, что попало в console, становится структурной записью: уровень, время, точка присутствия, идентификатор запроса. Хвост доступен сразу:

терминал
$ tessera logs --tail --pop dme1 --level warn
14:22:07.412  dme1  warn   KV miss для quote:RU        req_8f21ac
14:22:07.598  dme1  info   ответ 200, cpu 4.1 мс       req_8f21ac
14:22:09.004  fra1  error  апстрим 503, повтор 1/3     req_9b02de

Трейсы отдаются в формате OpenTelemetry — можно направить в свой коллектор:

tessera.json
{
  "observability": {
    "otlp": { "endpoint": "https://otel.example.ru:4318", "sample": 0.1 },
    "logs": { "retention_days": 7 }
  }
}

Метрики собираются по точкам присутствия: запросы, ошибки, p50/p95/p99 задержки, процессорное время, попадания в KV. Живая сводка — на странице статуса.

Лимиты

Полная таблица — на странице платформы. Здесь то, обо что чаще всего спотыкаются.

ЛимитЗначениеЧто происходит при превышении
Процессорное время50 мсзапрос обрывается, код 1102
Память изолята128 МБизолят снимается, код 1103
Размер бандла10 МБдеплой не проходит
Подзапросов fetch50fetch бросает исключение
Записей KV на ключ1/слишние записи отбрасываются молча
Как узнать, сколько CPU тратит функция tessera trace --last 20 --min-ms 40 покажет самые тяжёлые запросы с разбивкой по фазам. В песочнице то же самое видно на вкладке «Тайминги».

Коды ошибок

Платформенные ошибки приходят с кодом 11xx в заголовке x-tessera-error и в логах. Ошибки вашего кода — это обычные исключения, они попадают в лог целиком со стеком.

КодЧто случилосьЧто делать
1101Исключение в handlerсмотреть стек в tessera logs
1102Превышено процессорное времявынести тяжёлое в ctx.waitUntil или в WASM
1103Превышена памятьне собирать большие массивы в памяти, работать потоком
1104handler не вернул Responseпроверить все ветви, включая ранние return
1105Слишком много подзапросовобъединить запросы, кешировать в KV
1106Бандл не прошёл проверку подписипересобрать и задеплоить заново
1107Биндинг не найден в envдобавить в tessera.json и задеплоить
1108Тело запроса больше 100 МБчитать потоком или загружать в Blob напрямую
ответ при ошибке платформы
{
  "error": "cpu_time_exceeded",
  "code": 1102,
  "request_id": "req_8f21ac",
  "pop": "dme1",
  "hint": "функция израсходовала 50 мс CPU; проверьте циклы и JSON.parse больших тел"
}

API рантайма

Доступны стандартные Web API. Ниже — только то, что добавляет платформа.

env.KV

МетодВозвращаетЗаметки
get(key, type?)string | object | nulltype: "text" или "json"
put(key, value, opts?)voidopts.ttl в секундах, минимум 60
delete(key)voidидемпотентно
list(opts?){ keys, cursor }prefix, limit до 1000

env.COUNTERS

МетодВозвращаетЗаметки
incr(key, by?)numberатомарно в пределах точки
get(key)number0, если ключа нет
reset(key)voidсбрасывает в 0

env.BLOB

МетодВозвращаетЗаметки
put(key, stream, opts?){ etag, size }до 5 ГБ, поток или ArrayBuffer
get(key)Response | nullподдерживает range
head(key){ etag, size } | nullбез загрузки тела
signedUrl(key, ttl)stringпрямая отдача клиенту

ctx

МетодЗачем
waitUntil(promise)продлить жизнь изолята до завершения фоновой работы
passThroughOnException()при исключении отдать запрос апстриму, а не 500
propsданные, переданные из предыдущего звена цепочки

↑ справочник вымышленный: API придуман для этой заглушки и нигде не существует

Примеры лучше читать руками

Любой фрагмент отсюда можно вставить в песочницу и запустить: раннер настоящий, ошибки настоящие.