GitHub - beachdevs/vanilla-light: Full stack web framework

3 min read Original article ↗

vanilla-light

npm version runtime npm downloads

Vanilla-light is a no-build, dependency-free full-stack framework with a reactive browser client and an HTTPS Bun server.

Why I built this

This is the culmination of 20 years of writing, re-writing, imagining, and re-imagining what is the minimal core framework and best abstraction of the web client/server model.

Web standards give us all the tools we need to create applications. Removing complexity and the sheer number of things you have to learn makes them easier to build.

Most apps are really not that complex. They consist of a front-end, back-end server, auth, storage, and (lately) communication with llms. Sophisticated server and client rendering schemes, bloated thousand-dependency builds, and quirky frame dependent abstractions do not belong here.

Vanilla light is intended to be an easy to adopt minimal core for humans to build things quickly and go from imagination to application.

Features

  • Standalone front and back-ends
  • Can be hosted separately
  • No frontend build step
  • No runtime npm dependencies
  • Reactive window.state + custom web components
  • Plugin-driven backend (src/plugins)
  • Auth (auth0, bearer) | Db (jsonl) | llm (OpenAI-compatible providers)

Quick Start

Or clone this repo:

CLI

Manage server with:

npx vanilla-light <command>
-- or --
vlserver <command>

Common commands:

vlserver start
vlserver config
vlserver port 3000
vlserver insecure true
vlserver insecure false
vlserver certsdir certs
vlserver +plugin auth/bearer.js
vlserver -plugin auth/bearer.js

Configuration

~/.vanilla-light/config.json

{
  "use_plugins": [
    "always/logging.js",
    "auth/auth0.js",
    "auth/bearer.js",
    "storage/s3.js",
    "storage/kvfile.js",
    "llm/openai.js"
  ],
  "port": 3000,
  "disable_ssl": true,
  "certs_dir": "certs"
}

HTTPS should generally be used. Enable SSL, except when behind a reverse-proxy.

Frontend

The front-end consists of a client.js file, which contains helper functions to send/get key vals stored on the server.

Client import:

import { $, $$, get, set, del, me } from '/client.js'
-- or --
import { $, $$, get, set, del, me } from 'https://unpkg.com/vanilla-light/client.js'

Web components are defined and used in the simplest way. A components.js file contains their definitions.

All components are exported as components from components.js. A component is simply a function that returns (or renders html) and runs at page load.

export const components = {
  "simple-hello": () => `Hello world!`,
  "hello-world": {
    prop: (data) => `${data} World`,
    render: function(data) {
      return this.prop(data);
    }
  }
};

/client.js automatically imports /components.js when present.

Client server architecture

Client/server architecture

Server secrets

Plugins get their secrets (api keys, etc..) from env

AUTH0_DOMAIN=...
AUTH0_CLIENT_ID=...
AUTH0_CLIENT_SECRET=...
CLOUDFLARE_ACCESS_KEY_ID=...
CLOUDFLARE_SECRET_ACCESS_KEY=...
CLOUDFLARE_BUCKET_NAME=...
CLOUDFLARE_PUBLIC_URL=...
# Any OpenAI-compatible Chat Completions endpoint
LLM_ENDPOINT=https://api.openai.com/v1/chat/completions
# API key for whatever provider powers LLM_ENDPOINT (OpenAI, OpenRouter, etc.)
LLM_API_KEY=...

API Definition

Main routes: POST /register, POST /login GET /me POST/GET/DELETE /{key} PUT /{key} PROPFIND / PATCH /{key} POST /llm/chat

Writing a Server Plugin

File: src/plugins/<group>/<name>.js

import { json, redir } from '../../server.js'

export default function plugin(app) {
  app.routes = {
    ...app.routes,
    'GET /hello': async (req) => json({ hello: 'world', user: req.user?.sub || null }),
    'GET /go-home': async () => redir('/')
  }
}

Writing Custom Web Components

Define in public/components.js:

export const components = {
  'hello-card': (data) => `<div>Hello ${data || 'world'}</div>`,
  'user-badge': {
    render: async () => {
      const u = await window.me()
      return `<b>${u?.name || 'guest'}</b>`
    }
  }
}

Use in HTML:

<hello-card data="name"></hello-card>
<user-badge></user-badge>
<script>window.state.name = 'Chris'</script>

window.state Reactivity

window.state is a Proxy.

window.state.count = 1
-> proxy set(...)
-> find [data="count"]
-> render matching registered component

Tests

Run with server up:

bash test/server.sh
# optional
BASE=https://localhost:3000 bash test/server.sh