
My Impressions After Building an In-House AI Tool Full-Stack with SvelteKit — Fitting SSE, Playwright, and Encryption into One Framework
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
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
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
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.