I'm assuming you're running a Debian-based distro. I *Will not give detailed instructions for Windows/MacOS users. Check official documentation.
Install pnpm (if not already installed):
npm install -g pnpm
Invoke-WebRequest https://get.pnpm.io/install.ps1 -UseBasicParsing | Invoke-Expressionbrew install pnpmInstall nvm (Node Version Manager):
Ubuntu/Linux:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# Restart your terminal or run:
source ~/.bashrc
Note For Macs with the Apple Silicon chip, node started offering arm64 arch Darwin packages since v16.0.0 and experimental arm64 support when compiling from source since v14.17.0. If you are facing issues installing node using nvm, you may want to update to one of those versions or later.
Install and use Node 22:
nvm install 22
nvm use 22
Run Vite Server:
# from repo root
cd client
# install dependencies
pnpm install
# start dev server
pnpm run dev
BACKEND_URL - Backend API base URLBACKEND_SECRET - Shared secret for HMAC request signingPUBLIC_API_URL - Public-facing API URL (optional if using relative paths)NODE_ENV - Environment (development/production)Create a .env file in the client/ directory:
BACKEND_URL=http://localhost:8000
BACKEND_SECRET=your-secret-here
# build
pnpm run build
# preview production build locally
pnpm run preview
# or with vercel CLI from the client folder:
vercel --cwd . # preview (non-prod)
vercel --cwd . --prod # production
Notes:
client/. Use --cwd client when running Vercel CLI from repo root.pnpm is the recommended package manager for this project.Recommended Vercel settings for Git auto-deploys:
clientpnpm installpnpm run buildTo make Vercel only auto-deploy main as production (prevent develop builds), add an Ignored Build Step in Vercel Git settings:
bash -lc 'if [ "$VERCEL_GIT_COMMIT_REF" != "main" ]; then exit 0; fi; exit 1'
Manual CLI:
vercel --cwd clientvercel --cwd client --prod --confirm (or vercel --prod --cwd client --force)If you prefer CI-controlled production deployments, create a Deploy Hook in Vercel for the main branch and trigger it from your CI pipeline.
To keep images/fonts cached without forcing clients to reload, add a vercel.json with headers for:
/_app/immutable/* → public, max-age=31536000, immutable/fonts/* → public, max-age=31536000, immutable/images/* → public, max-age=604800, stale-while-revalidate=86400enhanced:img receiving undefined src in SSR<img> with fallback or ensure src is always definedcookies.set() includes path: '/', httpOnly: true, secure: true<label for=FORM_ELEMENT>QA Engineer Mr. Martin Marrero Will provide extensive documentation on testing.
During the development and as I became more comfortable working with SvelteKit (and most importantly, as I started to understand stuff better), I found better ways to do things, so you'll find garbage code everywhere and very confusing stuff, and I'm sorry for that, every backend request needs to be moved to strict server-side, some components are Jesus and they have some savage prop drilling, I'm aware there's a mess of client and server-side functions. So bear with me... If I find the time, I will refactor this shit.
Overview: the repo is split into a small repo root and a client app. The client is a SvelteKit app located in the client/ folder. The structure below reflects the recommended places for components, services, server-only code and routes.
Purpose: keep components grouped by UI scope/role so ownership, reuse and styling boundaries are clear. I don't know if this is meta in front-end development, I use it because it's starting to become a mess.
/app/**Key patterns
src/lib/server so it cannot accidentally be shipped to the browser.+server.ts files for internal API endpoints that handle backend communication. These act as a BFF (Backend-for-Frontend) layer.AuthBox in layouts (e.g., app/+layout.svelte, app/manager/+layout.svelte)./login, /welcome) outside the /app directory.index.ts inside scope folders to export the public surface (import { Sidebar } from '$lib/components/manager').signedJsonFetch, signedGetFetch, or signedMultipartFetch from authFetch.ts to add HMAC-SHA256 signatures.The application uses a two-stage authentication process involving a magic link for login and a JWT for managing user sessions. All communication with the backend is secured using HMAC-SHA256 request signatures.
This flow handles both new and returning users.
[Login Page] --(email)--> [+page.server.ts Action] --(signed req)--> [Backend API /auth/login]
Flow Details:
/login page.login/+page.server.ts action receives the email./auth/login endpoint.signup_token, and responds with { "isFirstTime": true, "signup_token": "..." }.{ "isFirstTime": false, "access_token": "..." }./app/graduate/upload_cv?token=...), passing the temporary signup_token. No session cookie is set at this stage.HttpOnly cookie named spotly_session and redirects the user to their dashboard (/app/graduate).This flow completes the registration for new users after they follow the magic link.
[Upload CV Page] --(files, token)--> [+page.server.ts Action] --(signed req)--> [Backend API /signup]
Flow Details:
/app/graduate/upload_cv page, submits their CVs. The form includes the signup_token from the URL.upload_cv/+page.server.ts action receives the files and the signup_token./signup endpoint, passing the signup_token in the URL.signup_token, creates the user account, saves the files, and generates a final JWT. It responds with { "access_token": "..." }.spotly_session cookie, officially logging the user in.This flow runs on every server-side navigation for a logged-in user.
[Browser Request] --(cookie)--> [hooks.server.ts] --(signed req, JWT)--> [Backend API /auth/me]
Flow Details:
hooks.server.ts file intercepts every incoming request.spotly_session cookie. If it doesn't exist, the user is considered logged out./auth/me endpoint, passing the JWT as a Bearer token.event.locals.user with this data, making it available to all server load functions and actions for that request.[Logout Button] --(POST)--> [/logout Endpoint]
Flow Details:
/logout server endpoint./logout/+server.ts endpoint deletes the spotly_session cookie./). The hooks.server.ts file will now treat them as a logged-out user on all subsequent requests.authFetch.ts)All server-to-backend communication is handled by a set of specialized fetch wrappers located in src/lib/server/authFetch.ts. These functions ensure that every request sent to the backend API is correctly signed according to the project's security protocol.
The base utility, signRequest, generates an HMAC-SHA256 signature from a payload and a timestamp using a shared BACKEND_SECRET. The message format for the signature is timestamp:payload.
signedJsonFetchThis is the standard fetch utility for all API calls that send and receive JSON data.
body to create the signature payload.Content-Type: application/json, X-Signature, and X-Timestamp headers to the request.Example Usage (from login/+page.server.ts):
import { signedJsonFetch } from '$lib/server/authFetch';
const body = { email: '[email protected]' };
const response = await signedJsonFetch(`${BACKEND_URL}/auth/login`, {
method: 'POST',
body: JSON.stringify(body)
});
signedMultipartFetchThis is a specialized fetch utility designed exclusively for file uploads (multipart/form-data).
FormData object cannot be read and stringified on the server. The backend must also expect an empty payload when validating a multipart request.Content-Type header. This allows the fetch API to automatically set the correct multipart/form-data header with the required boundary string.Example Usage (from upload_cv/+page.server.ts):
import { signedMultipartFetch } from '$lib/server/authFetch';
// `formData` is the FormData object from the request
const response = await signedMultipartFetch(
`${BACKEND_URL}/signup`,
{
method: 'POST',
body: formData
}
);
AuthBox is a tiny reusable wrapper that chooses what to render depending on the user's authentication status and role, derived from SvelteKit's $page store. It uses Svelte 5 snippets via @render so you can pass renderable content from the caller.
This document explains:
AuthBox expects the following props:
authorizedContent (required, Snippet): Rendered when the user is fully authorized (logged in and, if required, has the correct role).unauthorizedContent (optional, Snippet): Rendered when the user is logged out. If not provided, nothing is rendered for logged-out users.requiredRole (optional, 'manager' | 'graduate'): Specifies the role a user must have. If provided, the user must be logged in AND have this role to be authorized.This example shows or hides content based only on whether a user is logged in, without checking for a specific role.
<script>
import AuthBox from '$lib/components/main/utils/AuthBox.svelte';
</script>
<AuthBox>
{#snippet authorizedContent()}
<!-- This UI is shown to ANY logged-in user -->
<a href="/app/dashboard" class="btn">Dashboard</a>
{/snippet}
{#snippet unauthorizedContent()}
<!-- This UI is shown to logged-out users -->
<a href="/login" class="btn">Sign In</a>
{/snippet}
</AuthBox>
To protect content for a specific role, pass the requiredRole prop. The component handles the logic internally.
Behavior:
manager role, authorizedContent is rendered.graduate), the component automatically renders a built-in <Unauthorized /> error page.unauthorizedContent is rendered.<script>
import AuthBox from '$lib/components/main/utils/AuthBox.svelte';
import ManagerDashboard from '$lib/components/manager/ManagerDashboard.svelte';
</script>
<!-- This AuthBox protects the manager's dashboard -->
<AuthBox requiredRole={'manager'}>
{#snippet authorizedContent()}
<!-- This is only rendered for users with the 'manager' role -->
<ManagerDashboard />
{/snippet}
{#snippet unauthorizedContent()}
<!-- You can still provide a fallback for logged-out users -->
<p>Please log in to access the application.</p>
{/snippet}
</AuthBox>
$page.data.user, which is populated by SvelteKit's server load functions. This ensures the correct content is rendered on the server, preventing UI flashes on the client.requiredRole is specified and the user's role does not match, AuthBox automatically displays a generic Unauthorized component. You do not need to handle this case manually.authorizedContent and unauthorizedContent snippets.src/routes/app/manager/+layout.svelte) to protect entire sections of your application based on user roles.