My Impressions After Building an In-House AI Tool Full-Stack with SvelteKit — Fitting SSE, Playwright, and Encryption into One Framework

My Impressions After Building an In-House AI Tool Full-Stack with SvelteKit — Fitting SSE, Playwright, and Encryption into One Framework

I will summarize my firsthand experience building an in-house AI tool by integrating UI, API, SSE streaming, Playwright authentication, SQLite database, and internationalization into a single project, deployed with SvelteKit using the Node adapter.
2026.07.30

This page has been translated by machine translation. View original

Introduction

When building an internal AI prompt execution tool, I chose SvelteKit as the framework. Frontend UI, server-side API routes, Playwright-based browser authentication, SSE streaming, and SQLite database——everything is contained within a single SvelteKit project.

I'll summarize my hands-on experience using SvelteKit as a full-stack framework in the context of an internal tool.

Prerequisites & Environment

  • SvelteKit v2 + Svelte 5
  • @sveltejs/adapter-node (deployed as a Node server)
  • Drizzle ORM (SQLite)
  • Playwright (server-side authentication)
  • Paraglide.js (i18n)
  • TailwindCSS v4 + shadcn-svelte

Overall Architecture

src/
├── routes/
│   ├── +page.svelte          ← Main UI
│   ├── +layout.svelte         ← App shell
│   └── api/
│       ├── auth/              ← Authentication API
│       │   ├── login/
│       │   ├── logout/
│       │   └── status/
│       └── gpt/               ← AI API
│           ├── generate/      ← SSE streaming
│           ├── thread/
│           ├── chat/
│           └── assistant/
├── lib/
│   ├── client/                ← Browser-side code
│   │   ├── gptService.ts
│   │   ├── authService.ts
│   │   └── credentialStorage.ts
│   ├── server/                ← Server-side code
│   │   ├── auth/              ← Playwright authentication
│   │   ├── services/          ← AI API wrappers
│   │   └── db/                ← Drizzle ORM
│   ├── components/            ← UI components
│   ├── templates/             ← Template JSON
│   └── paraglide/             ← i18n runtime (auto-generated)
└── hooks.server.ts            ← Paraglide middleware

SvelteKit Features

API Routes and UI Coexist in the Same Project

src/routes/api/gpt/generate/+server.ts
import aiService from "$lib/server/services/aiService";

export const POST: RequestHandler = async ({ request }) => {
  const { prompt, name, instruction, webSearch } = await request.json();
  const response = await aiService.chat(name, instruction, prompt, !!webSearch);

  return new Response(response.data, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive"
    }
  });
};

Code located in $lib/server/ is executed only on the server side. Since SvelteKit guarantees this separation at build time, accidents where server code accidentally gets bundled into the client don't happen.

SSE Streaming Works with Standard Web APIs

SvelteKit API routes return the standard Web Response object. To return SSE streaming, you simply pass the stream to Response. No special libraries or middleware were needed.

Reactive State Management with Svelte 5 Runes

+page.svelte
let output = $state('');
let isLoading = $state(false);
let isStreaming = $state(false);
let selectedTemplate = $state<Template | null>(null);
let isParameterInputCollapsed = $state(false);

Svelte 5's $state rune is equivalent to React's useState, but allows direct assignment without setter methods. Managing UI state during streaming (loading, streaming, parameter collapsed state, etc.) can be written naturally.

Inserting Middleware with hooks.server.ts

hooks.server.ts
import { paraglideMiddleware } from '$lib/paraglide/server';

const handleParaglide: Handle = ({ event, resolve }) =>
  paraglideMiddleware(event.request, ({ request, locale }) => {
    event.request = request;
    return resolve(event, {
      transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
    });
  });

export const handle: Handle = handleParaglide;

Middleware that passes through all requests can be defined in hooks.server.ts. Here, Paraglide's locale detection is performed and the HTML lang attribute is set dynamically.

Challenges I Actually Encountered

Files Tend to Get Large

The main page's +page.svelte grew to 1,198 lines. With SvelteKit's file-based routing, each page maps to a single .svelte file, so as logic within a page grows, the file becomes bloated.

As a countermeasure, I split things into components (AuthSettings.svelte, TemplateManager.svelte, OutputDisplay.svelte, etc.), but page-level state management remains in +page.svelte.

CSS Scoping Issues with Component Libraries

When using component libraries like shadcn-svelte, they can conflict with Svelte's CSS scoping.

<!-- Applying a custom class to the library's Button component -->
<Button class="locale-switcher-btn">...</Button>

<style>
  /* NG: Scoped and doesn't reach the library's DOM */
  .locale-switcher-btn { background: rgba(255, 255, 255, 0.05); }

  /* OK: Remove scoping with :global() */
  :global(.locale-switcher-btn) { background: rgba(255, 255, 255, 0.05); }
</style>

There were frequent cases where the :global() wrapper was necessary.

Benefits and Constraints of Compile-time i18n

Paraglide.js generates TypeScript code from message files at build time.

import * as m from '$lib/paraglide/messages/_index.js';

// Type-safe, autocomplete works
error = m.please_fill_required({ fields: missingRequired.join(', ') });

Compared to runtime i18n libraries (i18next, etc.):

Item Paraglide (compile-time) i18next (runtime)
Type safety ○ Argument type checking × String keys
Bundle size ○ Only used locales △ All locales loaded
Flexibility △ Build required ○ Can add locales dynamically
Server-side ○ Handled via middleware

When supported languages are limited in an internal tool (English and Japanese only), the type safety of compile-time i18n is a significant advantage.

Scope of What SvelteKit Handles in a Single Project

Summarizing the roles SvelteKit plays in this project:

Role Technology
Frontend UI Svelte 5 + shadcn-svelte + TailwindCSS
API server SvelteKit server routes
SSE streaming Web Response API
Authentication proxy Playwright (server-side)
Database Drizzle ORM + SQLite
Internationalization Paraglide.js (compile-time)
Markdown rendering mdsvex
Deployment adapter-node → Node server

What traditionally would be split into two projects——"React frontend + Express backend"——is consolidated into a single SvelteKit project.

Conclusion

The results of using SvelteKit as a full-stack framework for an internal tool:

  • Server-side code separation is clear, allowing heavy dependencies like Playwright to be managed cleanly
  • SSE streaming could be written naturally with the Web standard API
  • State management became simpler with Svelte 5 runes
  • Type safety of compile-time i18n was well-suited for small-scale multilingual support
  • Page file bloat and CSS scoping issues require attention

Internal tools often involve "building quickly with a small team and operating it yourselves." SvelteKit, which allows managing both frontend and backend in a single project, felt well-suited for this purpose.

Share this article