npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@solcreek/svelte-adapter

v0.8.0

Published

SvelteKit deployment adapter for @solcreek/creekd self-host (Bun / Node runtime)

Readme

@solcreek/svelte-adapter

SvelteKit deployment adapter for @solcreek/creekd self-host. Produces a runnable HTTP server entry + a .creek-creekd/manifest.json describing the supervised process, so creekctl up --from .creek-creekd/manifest.json knows exactly what to spawn.

Pairs creekd's neutral process supervisor (cgroups, namespaces, dispatch, health probes) with SvelteKit-shaped defaults: prerendered fast-path, hashed-asset immutable caching, X-Forwarded-* aware URL rebuilding, sveltekit:shutdown graceful drain.

@sveltejs/adapter-node    /bench/slow      ⏤  191 req/s  ·  52 ms p50
@solcreek/svelte-adapter  /bench/slow      ⏤  191 req/s  ·  52 ms p50   (zero overhead)
@solcreek/svelte-adapter  /bench/cached    ⏤  11,680 req/s · 0.67 ms p50  ← platform.cache

pnpm bench reproduces these numbers on your machine. The cached path uses event.platform.cache (L1 LRU + L2 filesystem, survives restart, tag invalidation) — see Benchmark and platform.cache.

Status

Pre-1.0, targets SvelteKit ≥ 2.0. The adapter is feature-complete vs @sveltejs/adapter-node (same env-var surface, same sveltekit:shutdown contract, same node_modules-on-target deployment model) and adds platform.cache as its differentiator. Self-host, Bun runtime, and creekctl integration are first-class.

Install

pnpm add -D @solcreek/svelte-adapter

Usage

// svelte.config.js
import adapter from "@solcreek/svelte-adapter";

export default {
  kit: {
    adapter: adapter({
      runtime: "bun",   // default; "node" is the safe fallback
      port: 3000,
      // healthCheckPath: "/_creek/health",   // default
      // precompress: true,                   // default
      // env: { FEATURE_X: "1" },             // additive over NODE_ENV=production
    }),
  },
};

After pnpm build:

  • build/index.js — the runtime entry (Node or Bun)
  • build/server/, build/client/, build/prerendered/ — SvelteKit output
  • .creek-creekd/manifest.json — what creekd reads to spawn the process

Spawn under creekd:

creekctl up --from .creek-creekd/manifest.json

Options

| Option | Default | Notes | |---|---|---| | runtime | "bun" | "bun" | "node". Recorded in the manifest so creekd uses the right executable. | | port | 3000 | TCP port the entry binds to. Overridable at spawn via PORT. | | out | "build" | Output directory. | | env | { NODE_ENV: "production" } | Written into the creekd manifest; KEY=VALUE strings or an object. NODE_ENV defaults to production unless overridden. | | healthCheckPath | "/_creek/health" | Always 200 before SvelteKit sees the request. Also recorded in the manifest as creekd's liveness probe. | | precompress | true | gzip + brotli on client/ and prerendered/. | | bundle | false | false (deps stay in node_modules on target) or "esbuild" (self-contained index.js). See Bundling. | | fallback | unset | SPA / catch-all HTML filename (e.g. "200.html", "404.html"). See SPA / catch-all fallback. |

platform.cache — persistent KV for SvelteKit (the differentiator vs adapter-node)

SvelteKit doesn't ship a first-class ISR or cache-handler primitive — there's no equivalent of Next.js's cacheHandler. This adapter exposes a small persistent cache on event.platform.cache so user code can do tag-invalidated, restart-surviving caching without bolting on Redis just to self-host.

// src/routes/+page.server.ts
export async function load({ platform }) {
  return {
    feed: await platform.cache.cached(
      "homepage-feed:v1",
      { revalidate: 60, tags: ["feed"] },
      async () => {
        // expensive query — runs once per minute (or after invalidateTag)
        return await db.feed.recent();
      },
    ),
  };
}

// src/routes/api/publish/+server.ts
export async function POST({ platform, request }) {
  await db.posts.insert(await request.json());
  await platform.cache.invalidateTag("feed");
  return new Response(null, { status: 204 });
}

Implementation:

  • L1: in-process LRU (insertion-order Map; default 2048 entries)
  • L2: pluggable driver. bun-sqlite on Bun (single cache.sqlite with WAL); fs on Node (one JSON per entry under $CREEK_SVELTE_CACHE_DIR/entries/<hash[0:2]>/<hash>.json, atomic via tmp+rename). auto (default) picks the right one at startup.
  • Tags: per-tag invalidation sentinel { invalidatedAt }; entries are stale if any of their tags was invalidated after entry.createdAt. Stored as a tags row in sqlite, as tags/<safe>.json in fs.
  • SWR: cached() returns stale data while a background loader refreshes; coalesces concurrent misses for the same key
  • Dev parity: adapter.emulate() provides the same cache in vite dev and prerender via event.platform.cache — no if (import.meta.env.DEV) branches needed
  • Graceful shutdown: cache is closed (in-flight writes flushed; sqlite WAL checkpointed) after sveltekit:shutdown listeners run

L2 driver microbench (1000 set + 1000 cold-L2 get, Bun 1.3, M-series macOS):

| Driver | set/s | get/s | |---|---:|---:| | fs | 4,573 | 31,984 | | bun-sqlite | 28,082 | 232,518 |

~6× write throughput, ~7× read throughput. The fs driver isn't slow in absolute terms — sqlite just avoids the per-entry mkdir/tmp/rename syscall trio and WAL gives durable writes without per-write fsync. The fs driver remains the only choice on Node.

Cache env vars

| Var | Default | Effect | |---|---|---| | CREEK_SVELTE_CACHE_DIR | .creek/svelte-cache (relative to cwd) | L2 directory | | CREEK_SVELTE_CACHE_L1 | 2048 | L1 LRU capacity (entries) | | CREEK_SVELTE_CACHE_DRIVER | auto | Force a specific L2 driver: fs, bun-sqlite, or auto. Pinning to fs on Bun is the migration escape hatch if a sqlite issue surfaces. | | CREEK_SVELTE_CACHE_DISABLED | unset | When =1, skip L2 entirely (in-memory only) |

App.Platform typing

To get autocomplete on event.platform.cache, declare it in your project's src/app.d.ts:

import type { CreekdSvelteCache } from "@solcreek/svelte-adapter/runtime";

declare global {
  namespace App {
    interface Platform {
      cache: CreekdSvelteCache;
    }
  }
}
export {};

Runtime environment variables

The generated entry honours the same env vars as @sveltejs/adapter-node, so existing Svelte deployment knowledge transfers directly:

| Var | Default | Effect | |---|---|---| | PORT | from adapter({ port }) | TCP port | | HOST | 0.0.0.0 | bind address | | ORIGIN | unset | overrides the request URL's origin entirely (preferred when proxy headers can't be trusted) | | PROTOCOL_HEADER | unset | header to read for event.url.protocol (typically x-forwarded-proto) | | HOST_HEADER | unset | header to read for event.url.host (typically x-forwarded-host) | | PORT_HEADER | unset | header to read for port | | ADDRESS_HEADER | unset | header to read for getClientAddress() (typically x-forwarded-for) | | XFF_DEPTH | 1 | trusted-proxy depth when parsing comma-separated forwarded-for chains | | BODY_SIZE_LIMIT | 524288 (512 KB) | requests with Content-Length above this are rejected with 413 | | SHUTDOWN_TIMEOUT | 30 (seconds) | grace window after SIGTERM before forcing exit |

Graceful shutdown

The entry emits the sveltekit:shutdown event on SIGINT / SIGTERM with the signal name as the reason; listeners may return promises and they are all awaited before the socket is closed. Use this to close DB pools, flush queues, etc.

// instrumentation.server.ts (or any module loaded at startup)
process.on("sveltekit:shutdown", async (reason) => {
  await db.end();
});

SPA / catch-all fallback

For SPA-mode apps (everything client-rendered) or for a styled custom 404 page, pass a fallback filename. The adapter calls builder.generateFallback() to write the SvelteKit root layout as a static HTML shell into the prerendered output, and the server entry serves that file on any SSR 404:

// svelte.config.js
import adapter from "@solcreek/svelte-adapter";
export default {
  kit: { adapter: adapter({ fallback: "200.html" }) },
};

Status code follows the filename: 404.html → 404, anything else (200.html, index.html, …) → 200. Prerendered hits and successful SSR responses still win — fallback only fires when SSR returns 404.

Instrumentation (SvelteKit 2.31+)

If you provide src/instrumentation.server.ts, this adapter wraps the generated entry so the instrumentation module loads before any application code — required for OpenTelemetry auto-instrumentation, logger init, DB pool warm-up, etc. No configuration needed; enable the kit feature in svelte.config.js:

// svelte.config.js
export default {
  kit: {
    experimental: { instrumentation: { server: true } },
    adapter: adapter(),
  },
};
// src/instrumentation.server.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";

const sdk = new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] });
sdk.start();

Caveats inherited from kit's builder.instrument(): "live exports" do not work (none in this adapter's entry), and OTel auto-instrumentation needs top-level-await runtime support (Node 14.8+ / Bun — always satisfied here).

Benchmark vs @sveltejs/adapter-node

Same minimal SvelteKit fixture, same Node version, same hardware, 2000 requests at concurrency 10. /bench/slow simulates a 50 ms backend dependency; /bench/cached wraps it in platform.cache.cached.

| Adapter | Endpoint | req/s | p50 (ms) | p95 (ms) | p99 (ms) | |---|---|---:|---:|---:|---:| | @sveltejs/adapter-node | /bench/slow | 191 | 52.3 | 53.8 | 55.4 | | @solcreek/svelte-adapter | /bench/slow | 191 | 52.2 | 53.3 | 53.9 | | @solcreek/svelte-adapter | /bench/cached | 11,680 | 0.67 | 2.1 | 2.8 |

Two takeaways:

  1. No overhead vs adapter-node on the uncached path — identical throughput and latency, so adopting this adapter doesn't make anything slower.
  2. 78× faster p50 and 61× higher throughput on the cached path. The same backend dependency, the same SvelteKit Server, just one platform.cache.cached(...) call.

Run the benchmark yourself: pnpm bench (uses the fixture at test/fixtures/real-sveltekit/, takes ~30 s).

Deployment

Default mode (bundle: false) ships build/ + node_modules to the target, exactly like @sveltejs/adapter-node:

git pull
pnpm install --prod
pnpm build
creekctl up --from .creek-creekd/manifest.json

Bundling via esbuild

Opt in with adapter({ bundle: "esbuild" }). The adapter inlines @sveltejs/kit/node + polyfills + cache handler into a single build/index.js so the target needs no node_modules:

// svelte.config.js
import adapter from "@solcreek/svelte-adapter";
export default {
  kit: { adapter: adapter({ bundle: "esbuild" }) },
};

| | bundle: false | bundle: "esbuild" | |---|---:|---:| | build/index.js | 12 KB | 108 KB | | build/ total | 748 KB | 832 KB | | node_modules on target | required (~30 MB) | not needed | | Effective deploy artifact | ~30 MB | ~832 KB | | pnpm install on target | needed | skipped |

Numbers from test/fixtures/real-sveltekit/. Inline source maps are included so production stack traces remain readable.

What stays external:

  • ./server/* — kit's Server dynamically imports route modules relative to its own location, so the server bundle must stay on disk under build/server/. Inlining it would break route loading at runtime.
  • bun:sqlite — Bun built-in, resolved at runtime (no npm package).
  • node:* — Node built-ins.

What's not handled automatically: native modules outside the kit server bundle (better-sqlite3, sharp, @node-rs/*, prebuilt .node binaries). If your app needs these, stick with bundle: false for now — extension of the externals list is a later iteration.

Comparison with @sveltejs/adapter-node

| | @sveltejs/adapter-node | @solcreek/svelte-adapter | |---|---|---| | Env-var surface (PORT, HOST, ORIGIN, PROTOCOL_HEADER, BODY_SIZE_LIMIT, …) | ✓ | ✓ identical | | sveltekit:shutdown event contract | ✓ | ✓ identical | | node_modules on target | required | required by default, skippable with bundle: "esbuild" | | Bun runtime support | — | ✓ first-class (runtime: "bun") | | Polka HTTP server | yes | — direct node:http / Bun.serve, no extra dep | | Creekd process manifest (.creek-creekd/manifest.json) | — | ✓ creekctl up --from reads it | | Configurable health probe path baked into the entry | — | ✓ (default /_creek/health) | | Persistent KV on event.platform (platform.cache) | — | ✓ L1 LRU + L2 fs, tag invalidation, SWR | | Emulator.platform() for dev parity | — | ✓ same cache in vite dev / prerender |

On the apples-to-apples path (no caching), throughput and latency are identical — adopting this adapter doesn't make anything slower. See Benchmark for the numbers and methodology.

License

Apache-2.0