Post นี้เล่าเรื่องการสร้าง Open-source Alternative ของ Jira Kanban Board ด้วย Upstash, SvelteKit, และ Firebase Storage

What we'll be using - เครื่องมือที่ใช้
- SvelteKit (UI และ API Routes)
- Upstash (CRUD Operations)
- Tailwind CSS (Styling)
- Firebase Storage (Asset Storage: Images, PDFs, etc.)
- SvelteKit Auth by Auth.js
What you'll need - สิ่งที่ต้องมี
- Upstash Account สร้าง Database
- Firebase Account สร้าง Storage Container
- Google OAuth 2.0 Setup ได้ OAuth Credentials
Setting up Upstash Redis - ตั้งค่า Upstash Redis
Login Upstash ไป Tab Redis กด Create Database


สร้าง Database เสร็จ ไป Details Tab Scroll หา Connect your database Section Copy Content เก็บไว้

Scroll หา REST API Section เลือก .env Button Copy Content เก็บไว้

Setting up the Project - ตั้งค่าโปรเจกต์
Clone App Repo Follow Tutorial เรียนรู้ทุกอย่างใน Repo Fork Project:
git clone https://github.com/rishi-raj-jain/jira-sveltekit-firebase-storage-upstash-starter
cd jira-sveltekit-firebase-storage-upstash-starter
npm install
Clone เสร็จ สร้าง .env File ใส่ Items ที่ Save จากข้างบน:
# .env
# Obtained from Google OAuth 2.0 setup
# https://support.google.com/cloud/answer/6158849?hl=en
GOOGLE_ID="..."
GOOGLE_SECRET="..."
# SvelteKit Auth
AUTH_SECRET="..."
# A random 32 char string
AUTH_TRUST_HOST=true
# Obtained from Upstash as from the steps done above
UPSTASH_REDIS_REST_URL="your_upstash_redis_rest__url_from_above"
UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest__token_from_above"
// firebase-adminsdk.json
// with the firebase config obtained from your firebase project
// Read more about firebase config
// https://firebase.google.com/docs/web/learn-more#config-object
{
"type": "...",
"project_id": "...",
"private_key_id": "...",
"private_key": "...",
"client_email": "...",
"client_id": "...",
"auth_uri": "...",
"token_uri": "...",
"auth_provider_x509_cert_url": "...",
"client_x509_cert_url": "...",
"universe_domain": "...",
"storageBucket": "..."
}
หลัง Steps นี้ รัน Local Environment ได้:
npm run dev
Repository Structure - โครงสร้าง Repository
Main Folder Structure โปรเจกต์ Highlight ไฟล์สีแดง ที่จะ Discuss Further เกี่ยว CRUD Operations, SvelteKit Auth, File Upload Handler และไฟล์ที่ Reference

Protecting SvelteKit's Edge Function By User Authentication - ป้องกัน Edge Function ด้วย User Authentication
งานเยี่ยมจาก Team Auth.js ทำ Auth กับ SvelteKit Seamless Project Implement:
Authorization on All Pages Using Google OAuth 2.0
ใช้ SvelteKit Server Hooks Enforce Auth ทุก Incoming Requests (ทุก Page):
// File: @/hooks.server.ts
import Google from "@auth/core/providers/google";
import { SvelteKitAuth } from "@auth/sveltekit";
import type { Handle } from "@sveltejs/kit";
import { GOOGLE_ID, GOOGLE_SECRET } from "$env/static/private";
// Read more on https://kit.svelte.dev/docs/hooks#server-hooks-handle
export const handle = SvelteKitAuth({
// @ts-ignore
providers: [Google({ clientId: GOOGLE_ID, clientSecret: GOOGLE_SECRET })],
}) satisfies Handle;
Authorization on Edge Functions Using SvelteKit's Server Locals
ใช้ SvelteKit Server Locals Opt-in Check User Authenticated ใน Server-side Operations ตัวอย่าง Validate User Authenticated ขณะ Create New Issue:
import { json } from '@sveltejs/kit'
import { isAuth } from '@/lib/auth'
import type { RequestEvent } from './$types'
import { getTask, getTasks } from '@/lib/issues'
import type { LayoutServerLoadEvent } from '../routes/$types'
import type { RequestEvent, ServerLoadEvent } from '@sveltejs/kit'
// Get user session if available in event locals
const isAuth = async (event: LayoutServerLoadEvent | ServerLoadEvent | RequestEvent) => {
const session = await event.locals.getSession()
if (session?.user?.image) { return { session } }
return false
}
export async function GET(event: RequestEvent) {
// If user not authenticated throw 403
if (!(await isAuth(event))) { return new Response(undefined, { status: 403 }) }
const url = event.url
const idSearchParam = url.searchParams.get('id')
if (idSearchParam) { const res = await getTask(idSearchParam); return json(res) }
else if (url.searchParams.get('all')) { const res = await getTasks(); return json(res) }
return new Response(JSON.stringify({ code: 0, error: 'Invalid Request.' }), { status: 400, headers: { 'content-type': 'application/json' } })
}
Issue(s) CRUD Operations via Upstash Redis - CRUD Operations ผ่าน Upstash Redis
Section นี้ Deep Dive Data Fetching, Updating, Deletion สำหรับแต่ละ Issue บน Kanban Board ใช้ Upstash DB (via @upstash/redis) Fetch, Display, Refresh Data ตลอด
getTask: Fetching Issue Data Function
getTask Function ใช้ Upstash hget via id as Key API Request ไป Upstash ขอ Issue Data ระบุโดย Unique id หาก Issue ไม่มี (หรือ Error) Return Object { code: 0 } Redirect User ไป 404 (Issue Not Found) Auto ใน SvelteKit Dynamic Route
type Task = { [key: string]: any } | null;
// Get Issue Data
// File: @/lib/issues/get.ts
export async function getTask(id: string) {
try {
const redis = (await import("../upstash/setup")).default;
const task: Task = await redis.hget("issues", id);
if (!task) { return { code: 0, error: "No such issue found.", }; }
return { ...task, code: 1 };
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return { code: 0, error, };
}
}
CRUD Operations ที่เหลือ:
Create Issue
// Create Issue
// File: @/lib/issues/create.ts
export async function createTask(info: any) {
try {
const redis = (await import("../upstash/setup")).default;
const id = Math.random().toString().slice(2) + new Date().getUTCMilliseconds();
await redis.hset("issues", { [id]: info });
return { code: 1, id, message: "Issue Created Succesfully ✅" };
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return { code: 0, error, };
}
}
Delete Issue
// Delete Issue
// File: @/lib/issues/delete.ts
export async function deleteTask(id: string) {
try {
const redis = (await import("../upstash/setup")).default;
await redis.hdel("issues", id);
return { code: 1, message: "Deleted Succesfully!" };
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return { code: 0, error, };
}
}
Update Issue Data
// Update Issue Data
// File: @/lib/issues/update.ts
export async function updateTask(info: any, id: string) {
try {
const redis = (await import("../upstash/setup")).default;
if (id) {
const task = await redis.hget("issues", id);
if (task) { await redis.hset("issues", { [id]: info }); return { code: 1, message: "Updated Successfully" }; }
}
return { code: 0, error: "No such issue was found.", };
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return { code: 0, error, };
}
}
Rate Limiting - การจำกัดอัตรา
Implement Rate-limiting at the Edge ใช้ Upstash Redis Database Client และ Rate Limiter Library @upstash/ratelimit
// Reference Function to ratelimiting
// File: @/lib/upstash/ratelimit.ts
import { Ratelimit } from "@upstash/ratelimit";
import redis from "./setup";
export const ratelimit = {
upload: new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(2, "60s") }),
issues: new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(5, "60s") }),
};
Rate Limiting ทำได้:
A. Limit Issue Creation Per User Per Minute
Limit สร้าง 5 Issues ต่อ Authenticated User ต่อนาที Enforce Rate Limit โดย User Email ของ Authenticated User
// File: @/routes/api/issue/+server.ts
// Issue Creation POST API SvelteKit Handler
import { ratelimit } from "@/lib/upstash/ratelimit";
export async function POST(event: RequestEvent) {
const user = await isAuth(event);
if (!user) { return new Response(undefined, { status: 403 }); }
if (user.session.user?.email) {
const result = await ratelimit.issues.limit(user.session.user.email);
if (!result.success) { return new Response( JSON.stringify({ code: 0, error: `You can't create more than 5 issues per minute.` }), { status: 403, headers: { "content-type": "application/json" } } ); }
const { info } = await event.request.json();
const res = await createTask(info);
return json(res);
}
return new Response(undefined, { status: 403 });
}
B. Limit File Uploads Per User Per Issue Per Minute
Limit File Uploads 2 ต่อ Authenticated User ต่อ Task ต่อนาที Enforce โดย User Email + Task ID Upload สำเร็จ → Update Task ใน Upstash DB Append File URL
// File: @/routes/api/content/+server.ts
// File Upload POST API SvelteKit Handler
import { ratelimit } from "@/lib/upstash/ratelimit";
export async function POST(event: RequestEvent) {
// User Authentication Code
if (user.session.user?.email) {
// Validate User, Task ID and if file uploaded
// Rate limit 2 uploads per minute
const result = await ratelimit.upload.limit(`${user.session.user.email}_${taskID}`);
if (!result.success) { return new Response( JSON.stringify({ code: 0, error: `You can't upload more than 2 files per issue per minute.` }), { status: 403, headers: { "content-type": "application/json" } } ); }
// File upload code - Continue reading blog
}
return new Response(undefined, { status: 403 });
}
Handling File Uploads and Downloads with Firebase Storage - จัดการ File Upload/Download ด้วย Firebase Storage
Deep Dive Issue File Uploads/Downloads แบบ Secure และ Authenticated บน SvelteKit Edge ใช้ Firebase (v9) Storage Fetch/Upload Files
Ooh, but why not Cloudflare R2 for storage?
Community Advocate Cloudflare R2 Free Storage Plan และ Advantages แต่จุดที่ทำให้ห่วง: ต้องใส่ Credit Card ก่อนลองระบบ ทำให้มองหา Storage Solutions อื่น Firebase Storage ให้ 5 GB Free Storage หากเกิน Services จะหยุด แทนที่จะ Charge Credit Card โดยไม่ได้ Approve
SvelteKit Edge Function Upload Files to Firebase Storage
Edge Function นี้ Look at POST Request Event หาก User Authenticated ได้ taskID และ file จาก FormData Evaluate File Size < 5 MB Create Unique ID สร้าง Firebase Reference Unique Folder Upload File Firebase Return URL Access File Append URL นี้ไป files Key ของ Issue Data
// File: @/routes/api/content/+server.ts
// File Upload POST API SvelteKit Handler
import { initializeApp } from "firebase/app";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
import fireBaseConfig from "../../../../firebase-adminsdk.json";
export async function POST(event: RequestEvent) {
// User Authentication Code
if (user.session.user?.email) {
const app = initializeApp(fireBaseConfig);
const storage = getStorage(app);
const data = await event.request.formData();
const taskID = data.get("taskID");
const file = data.get("file");
// ...Validate User, Task ID and if a file is uploaded
// ...Rate Limiting Code
// File Size Restriction(s)
if (file.size > 5 * 1024 * 1024) { return new Response( JSON.stringify({ code: 0, error: "File size exceeds the limit of 5 MB." }), { status: 400, headers: { "content-type": "application/json" } } ); }
// Start File Upload Code
try {
const fileId = uuidv4();
if (!(file instanceof File)) return;
const storageRef = ref(storage, `uploads/${fileId}/${file.name}`);
const fileBuffer = await file.arrayBuffer();
const { metadata } = await uploadBytes(storageRef, new Uint8Array(fileBuffer));
const { fullPath } = metadata;
if (!fullPath) { return new Response( JSON.stringify({ code: 0, error: `<span>There was some error while uploading the file.</span> <span class="mt-1 text-xs text-gray-500">Report an issue with the current URL that you are on and with the code XXX.</span>` }), { status: 403, headers: { "content-type": "application/json" } } ); }
// If file uploaded successfully, append file to list of attachments to issue's data
const { code, ...taskValues } = await getTask(taskID);
if (code === 1) {
if (taskValues) {
if (taskValues.hasOwnProperty("files")) { taskValues["files"].push(`https://storage.googleapis.com/${storageRef.bucket}/${storageRef.fullPath}`); }
else { taskValues["files"] = [`https://storage.googleapis.com/${storageRef.bucket}/${storageRef.fullPath}`]; }
}
await updateTask(taskValues, taskID);
}
return json({ code: 1, message: "Uploaded Successfully" });
} catch (error) { return new Response( JSON.stringify({ code: 0, error: error.message || error.toString() }), { status: 403, headers: { "content-type": "application/json" } } ); }
}
return new Response(undefined, { status: 403 });
}
SvelteKit Edge Function Download File's Public URL from Firebase Storage
จำได้ไหม Append Unique URL จาก Firebase ไป Issue files Key Receive Unique URL as Image Parameter GET Request ไป SvelteKit Edge Function Retrieve Original File ใช้ getDownloadURL จาก Firebase Library ได้ Public URL Original Media
// File: @/routes/api/content/+server.ts
// File Upload GET API SvelteKit Handler
import { initializeApp } from "firebase/app";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
import fireBaseConfig from "../../../../firebase-adminsdk.json";
export async function GET(event: RequestEvent) {
if (!(await isAuth(event))) { return new Response(undefined, { status: 403 }); }
const url = event.url;
const image = url.searchParams.get("image");
if (image) {
try {
const app = initializeApp(fireBaseConfig);
const storage = getStorage(app);
const fileRef = ref(storage, image);
const imagePublicURL = await getDownloadURL(fileRef);
return json({ code: 1, image: imagePublicURL });
} catch (error) { return new Response( JSON.stringify({ code: 0, error: error.message || error.toString() }), { status: 500, headers: { "content-type": "application/json" } } ); }
}
return new Response(JSON.stringify({ code: 0, error: "Invalid Request." }), { status: 400, headers: { "content-type": "application/json" } });
}
Multiple Media Uploads Handle Image vs Video Frontend:
<!-- File: @/routes/issue/[slug]/+page.svelte -->
{#each fieldFiles as file}
<div class="mt-8 w-full border border-white/25 p-3">
{#if /\.(mp4|mov|mkv)/i.test(file)}
<video class="h-auto w-full" src="{file}" controls> <track kind="captions" /> </video>
{:else}
<img alt="{file}" src="{file}" class="h-auto w-full" />
{/if}
</div>
{/each}
But why an open source alternative to Jira Kanban Board? - ทำไมต้อง Open-source Alternative?
Benefits เลือก Open-source แทน Paid Solutions:
- Cost Savings: ไม่มี Licensing Fees ต่างกับ Jira
- Unlimited Customisability: Full Control Codebase Customize ตาม Needs ไม่ Limited Customisation
- Ease in Integrations: Leverage APIs Connect กับ Project Management, Version Control, Notification Services Open Source Nature Allow Developers Extend Functionality Create Plugins/Integrations
Conclusion - สรุป
Project ให้ Experience ค่า Implement Granular Rate Limiting, CRUD Data Operations, Firebase Storage APIs Get/Upload Files ทั้งหมดที่ Edge ด้วย Upstash @upstash/redis Library!