Turn your Svelte, React, Angular, or Vue frontend into a single C++ header file. Serve beautiful web interfaces directly from ESP32/ESP8266 flash memory with automatic gzip compression, ETag caching, and seamless OTA updates.

The problem: Traditional approaches like SPIFFS and LittleFS require separate partition uploads, complex OTA workflows, and manual compression. Your users end up managing multiple files, and your CI/CD pipeline becomes a mess.
The solution: SvelteESP32 compiles your entire web application into a single C++ header file. One firmware binary. One OTA update. Done.
| Feature | SvelteESP32 | SPIFFS / LittleFS |
|---|---|---|
| Single Binary OTA | ✓ Everything in firmware | ✗ Separate partition upload required |
| Gzip Compression | ✓ Automatic at build time | Manual or runtime compression |
| ETag Support | ✓ Built-in SHA256 + 304 responses | Manual implementation required |
| CI/CD Integration | ✓ One npm command | Complex upload_fs tooling |
| Memory Efficiency | Flash only (PROGMEM/const arrays) | Filesystem partition + overhead |
| Performance | Direct byte array serving | Filesystem read latency |
| Setup Complexity | Include header, call one function | Partition tables, upload tools, handlers |
Best for: Single-binary OTA, CI/CD pipelines, static web UIs that ship with firmware.
Consider SPIFFS/LittleFS for: User-uploadable files, runtime-editable configs, dynamic content.
npm install -D svelteesp32
After building your frontend (Vite/Rollup/Webpack):
npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=always
Include in your ESP32 project:
#include <PsychicHttp.h>
#include "svelteesp32.h"
PsychicHttpServer server;
void setup() {
server.listen(80);
initSvelteStaticFiles(&server);
}
That's it. Your entire web app is now embedded and ready to serve.
Just want production-safe defaults?
bash npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h \ --etag=always --gzip=always --cachetimehtml=0 --cachetimeassets=31536000ETags for instant 304s, gzip for smaller transfers,
no-cachefor HTML so updates are always picked up, and 1-year caching for content-hashed JS/CSS assets.
handlebars, picomatch, and mime-types dependencies; C++ generation is now pure TypeScript with a built-in MIME type map and direct tinyglobby exclude handling. --cachetime-html → --cachetimehtml, --cachetime-assets → --cachetimeassets (CLI now matches RC file keys); --dry-run alias removed — use --dryrun. SVELTEESP32_URI_HANDLERS/SVELTEESP32_MAX_URI_HANDLERS (psychic/espidf) now reflect the exact registered route count, including the default / route and --spa catch-allimport { svelteESP32 } from 'svelteesp32/vite') generates the header automatically after every build — call with no argument for RC file mode or pass an options object for plugin-options mode; npx svelteesp32 init interactive RC file wizard; Node.js >= 22 required--analyze for CI size budget checks (per-file table, exits 1 on over-budget); --manifest to write a companion JSON manifest alongside the header--cachetimehtml and --cachetimeassets for per-type cache control (e.g. no-cache for HTML, 1-year for content-hashed JS/CSS)--spa) for client-side routers on all four engines-e webserver), dependency updatespsychic engine. The psychic2 engine has been removed. Dry run mode, C++ identifier validation, improved MIME type warnings--maxsize, --maxgzipsize)--basepath for multiple frontends (e.g., /admin, /app)npm install -D svelteesp32
initThe init command creates a .svelteesp32rc.json configuration file interactively so you never have to remember CLI flags:
npx svelteesp32 init
It asks for engine, source path, output path, and ETag preference, writes the RC file, and optionally runs the tool immediately.
For Vite-based projects (SvelteKit, React, Vue, Vanilla) you can skip the manual CLI step entirely — the plugin hooks into the build pipeline and regenerates the C++ header automatically after every vite build.
The plugin has two exclusive modes — pick one:
RC file mode — call with no argument (or a string path to a custom RC file). All settings come from .svelteesp32rc.json; outputfile in the RC file is required.
import { svelteKit } from '@sveltejs/kit/vite';
import { svelteESP32 } from 'svelteesp32/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
svelteKit(),
svelteESP32() // auto-discover .svelteesp32rc.json
// svelteESP32('/path/to/custom.rc.json') // or specify path explicitly
]
});
Plugin options mode — call with an options object. The RC file is completely ignored; output is required.
export default defineConfig({
plugins: [
svelteKit(),
svelteESP32({
output: '../firmware/include/svelteesp32.h',
engine: 'psychic',
etag: 'always',
gzip: 'always',
cachetimehtml: 0,
cachetimeassets: 31536000
})
]
});
sourcepath defaults to Vite's build.outDir in both modes.
Plugin options
| Option | Type | Default | Description |
|---|---|---|---|
output |
string |
RC outputfile |
Output .h file path |
sourcepath |
string |
Vite's build.outDir |
Source directory (compiled web files) |
engine |
'psychic'\|'async'\|'espidf'\|'webserver' |
'psychic' |
Target web server engine |
etag |
'always'\|'never'\|'compiler' |
'never' |
ETag generation mode |
gzip |
'always'\|'never'\|'compiler' |
'always' |
Gzip compression mode |
cachetime |
number |
0 |
Cache-Control: max-age in seconds (all files) |
cachetimehtml |
number |
(unset) | max-age for HTML files (overrides cachetime) |
cachetimeassets |
number |
(unset) | max-age for non-HTML files (overrides cachetime) |
exclude |
string[] |
[] |
Glob patterns to exclude |
basepath |
string |
(none) | URL prefix for all routes |
espmethod |
string |
'initSvelteStaticFiles' |
Generated init function name |
define |
string |
'SVELTEESP32' |
C++ #define prefix |
version |
string |
(none) | Version string embedded in header |
created |
boolean |
false |
Include creation timestamp |
spa |
boolean |
false |
Serve index.html for unmatched routes |
manifest |
boolean |
false |
Write companion .manifest.json |
noindexcheck |
boolean |
false |
Skip index.html validation |
maxsize |
number |
(none) | Max total uncompressed size in bytes |
maxgzipsize |
number |
(none) | Max total gzip size in bytes |
Choose your web server engine:
# PsychicHttpServer (recommended for ESP32)
npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=always
# ESPAsyncWebServer (ESP32 + ESP8266)
npx svelteesp32 -e async -s ./dist -o ./esp32/svelteesp32.h --etag=always
# Arduino WebServer (ESP32, synchronous, no dependencies)
npx svelteesp32 -e webserver -s ./dist -o ./esp32/svelteesp32.h --etag=always
# Native ESP-IDF
npx svelteesp32 -e espidf -s ./dist -o ./esp32/svelteesp32.h --etag=always
Watch your files get optimized in real-time:
[assets/index-KwubEIf-.js] ✓ gzip used (38850 -> 12547 = 32%)
[assets/index-Soe6cpLA.css] ✓ gzip used (32494 -> 5368 = 17%)
[favicon.png] x gzip unused (33249 -> 33282 = 100%)
[index.html] x gzip unused (too small) (472 -> 308 = 65%)
[roboto_regular.json] ✓ gzip used (363757 -> 93567 = 26%)
5 files, 458kB original size, 142kB gzip size
../../../Arduino/EspSvelte/svelteesp32.h 842kB size
Automatic optimizations:
PsychicHttpServer V2 (Recommended)
#include <PsychicHttp.h>
#include "svelteesp32.h"
PsychicHttpServer server;
void setup() {
server.listen(80);
initSvelteStaticFiles(&server); // One line. Done.
}
ESPAsyncWebServer
#include <ESPAsyncWebServer.h>
#include "svelteesp32.h"
AsyncWebServer server(80);
void setup() {
initSvelteStaticFiles(&server);
server.begin();
}
Arduino WebServer (built-in, no dependencies)
#include <WebServer.h>
#include "svelteesp32.h"
WebServer server(80);
void setup() {
initSvelteStaticFiles(&server);
server.begin();
}
void loop() {
server.handleClient();
}
Native ESP-IDF
#include <esp_http_server.h>
#include "svelteesp32.h"
httpd_handle_t server = NULL;
void app_main() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_start(&server, &config);
initSvelteStaticFiles(server);
}
Working examples with LED control via web interface: Arduino/PlatformIO | ESP-IDF
The generated header file includes everything your ESP needs:
```c //engine: PsychicHttpServer V2 //config: engine=psychic sourcepath=./dist outputfile=./output.h etag=always gzip=always cachetime=0 espmethod=initSvelteStaticFiles define=SVELTEESP32 //
...
static const uint8_t datagzip_assets_index_KwubEIf__js[12547] = {0x1f, 0x8b, 0x8, 0x0, ... static const uint8_t datagzip_assets_index_Soe6cpLA_css[5368] = {0x1f, 0x8b, 0x8, 0x0, 0x0, ... static const char etag_assets_index_KwubEIf__js[] = "387b88e345cc56ef9091..."; static const char etag_assets_index_Soe6cpLA_css[] = "d4f23bc45ef67890ab12...";
// File manifest for runtime introspection struct SVELTEESP32_FileInfo { const char path; uint32_t size; uint32_t gzipSize; const char etag; const char* contentType; }; const SVELTEESP32_FileInfo SVELTEESP32_FILES[] = { { "/assets/index-KwubEIf-.js", 38850, 12547, etag_assets_index_KwubEIf__js, "text/javascript" }, { "/assets/index-Soe6cpLA.css", 32494, 5368, etag_assets_index_Soe6cpLA_css, "text/css" }, ... }; const size_t SVELTEESP32_FILE_COUNT = si
$ claude mcp add svelteesp32 \
-- python -m otcore.mcp_server <graph>