the best lightweight web dev stack built on bun
1import indexHTML from "./pages/index.html";
2import {
3 createSession,
4 createUser,
5 deleteSession,
6 getUserBySession,
7 getUserByUsername,
8 getSessionFromRequest,
9} from "./lib/auth";
10import {
11 createAuthenticationOptions,
12 createRegistrationOptions,
13 deletePasskey,
14 getPasskeysForUser,
15 updatePasskeyName,
16 verifyAndAuthenticatePasskey,
17 verifyAndCreatePasskey,
18} from "./lib/passkey";
19import { requireAuth } from "./lib/middleware";
20import {
21 decrementCounter,
22 getCounterForUser,
23 incrementCounter,
24 resetCounter,
25} from "./lib/counter";
26
27const port = 3000;
28
29Bun.serve({
30 port,
31 routes: {
32 "/": indexHTML,
33
34 // Auth endpoints
35 "/api/auth/me": {
36 GET: (req) => {
37 try {
38 const sessionId = getSessionFromRequest(req);
39 if (!sessionId) {
40 return new Response(
41 JSON.stringify({ error: "Not authenticated" }),
42 {
43 status: 401,
44 },
45 );
46 }
47
48 const user = getUserBySession(sessionId);
49 if (!user) {
50 return new Response(JSON.stringify({ error: "Invalid session" }), {
51 status: 401,
52 });
53 }
54
55 return new Response(JSON.stringify(user), {
56 headers: { "Content-Type": "application/json" },
57 });
58 } catch (error) {
59 return new Response(
60 JSON.stringify({
61 error: error instanceof Error ? error.message : "Unknown error",
62 }),
63 { status: 500 },
64 );
65 }
66 },
67 },
68
69 "/api/auth/check-email": {
70 GET: (req) => {
71 try {
72 const url = new URL(req.url);
73 const username = url.searchParams.get("username");
74
75 if (!username) {
76 return new Response(
77 JSON.stringify({ error: "Username required" }),
78 {
79 status: 400,
80 },
81 );
82 }
83
84 const existing = getUserByUsername(username);
85 if (existing) {
86 return new Response(
87 JSON.stringify({ error: "Username already taken" }),
88 { status: 400 },
89 );
90 }
91
92 return new Response(JSON.stringify({ available: true }), {
93 headers: { "Content-Type": "application/json" },
94 });
95 } catch (error) {
96 return new Response(
97 JSON.stringify({
98 error: error instanceof Error ? error.message : "Unknown error",
99 }),
100 { status: 500 },
101 );
102 }
103 },
104 },
105
106 "/api/auth/register": {
107 POST: async (req) => {
108 try {
109 const body = await req.json();
110 const { username, credential, challenge } = body;
111
112 if (!username || !credential || !challenge) {
113 return new Response(
114 JSON.stringify({
115 error: "Username, credential, and challenge required",
116 }),
117 { status: 400 },
118 );
119 }
120
121 // Check if user already exists
122 const existing = getUserByUsername(username);
123 if (existing) {
124 return new Response(
125 JSON.stringify({ error: "Username already taken" }),
126 { status: 400 },
127 );
128 }
129
130 // Create user
131 const user = await createUser(username);
132
133 // Verify and create passkey
134 await verifyAndCreatePasskey(user.id, credential, challenge);
135
136 // Create session
137 const sessionId = createSession(user.id);
138
139 return new Response(JSON.stringify(user), {
140 headers: {
141 "Content-Type": "application/json",
142 "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${7 * 24 * 60 * 60}`,
143 },
144 });
145 } catch (error) {
146 return new Response(
147 JSON.stringify({
148 error: error instanceof Error ? error.message : "Unknown error",
149 }),
150 { status: 500 },
151 );
152 }
153 },
154 },
155
156 "/api/auth/logout": {
157 POST: (req) => {
158 try {
159 const sessionId = getSessionFromRequest(req);
160 if (sessionId) {
161 deleteSession(sessionId);
162 }
163
164 return new Response(JSON.stringify({ success: true }), {
165 headers: {
166 "Content-Type": "application/json",
167 "Set-Cookie":
168 "session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0",
169 },
170 });
171 } catch (error) {
172 return new Response(
173 JSON.stringify({
174 error: error instanceof Error ? error.message : "Unknown error",
175 }),
176 { status: 500 },
177 );
178 }
179 },
180 },
181
182 // Passkey endpoints
183 "/api/auth/passkey/register/options": {
184 GET: async (req) => {
185 try {
186 // For registration, we need username from query params (no session yet)
187 const url = new URL(req.url);
188 const username = url.searchParams.get("username");
189
190 if (!username) {
191 return new Response(
192 JSON.stringify({ error: "Username required" }),
193 {
194 status: 400,
195 },
196 );
197 }
198
199 // Create temporary user object for registration options
200 const tempUser = {
201 id: 0, // Temporary ID
202 username,
203 name: null,
204 avatar: "temp",
205 created_at: Math.floor(Date.now() / 1000),
206 };
207
208 const options = await createRegistrationOptions(tempUser);
209
210 return new Response(JSON.stringify(options), {
211 headers: { "Content-Type": "application/json" },
212 });
213 } catch (error) {
214 return new Response(
215 JSON.stringify({
216 error: error instanceof Error ? error.message : "Unknown error",
217 }),
218 { status: 500 },
219 );
220 }
221 },
222 },
223
224 "/api/auth/passkey/authenticate/options": {
225 GET: async (req) => {
226 try {
227 const options = await createAuthenticationOptions();
228
229 return new Response(JSON.stringify(options), {
230 headers: { "Content-Type": "application/json" },
231 });
232 } catch (error) {
233 return new Response(
234 JSON.stringify({
235 error: error instanceof Error ? error.message : "Unknown error",
236 }),
237 { status: 500 },
238 );
239 }
240 },
241 },
242
243 "/api/auth/passkey/authenticate/verify": {
244 POST: async (req) => {
245 try {
246 const body = await req.json();
247 const { credential, challenge } = body;
248
249 if (!credential || !challenge) {
250 return new Response(
251 JSON.stringify({ error: "Credential and challenge required" }),
252 { status: 400 },
253 );
254 }
255
256 const { userId } = await verifyAndAuthenticatePasskey(
257 credential,
258 challenge,
259 );
260
261 const user = getUserBySession(
262 createSession(
263 userId,
264 req.headers.get("x-forwarded-for") || undefined,
265 ),
266 );
267
268 if (!user) {
269 return new Response(JSON.stringify({ error: "User not found" }), {
270 status: 404,
271 });
272 }
273
274 const sessionId = createSession(userId);
275
276 return new Response(JSON.stringify(user), {
277 headers: {
278 "Content-Type": "application/json",
279 "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${7 * 24 * 60 * 60}`,
280 },
281 });
282 } catch (error) {
283 return new Response(
284 JSON.stringify({
285 error: error instanceof Error ? error.message : "Unknown error",
286 }),
287 { status: 500 },
288 );
289 }
290 },
291 },
292
293 // Counter endpoints
294 "/api/counter": {
295 GET: async (req) => {
296 try {
297 const userId = await requireAuth(req);
298 const count = getCounterForUser(userId);
299
300 return new Response(JSON.stringify({ count }), {
301 headers: { "Content-Type": "application/json" },
302 });
303 } catch (error) {
304 return new Response(
305 JSON.stringify({
306 error:
307 error instanceof Error ? error.message : "Not authenticated",
308 }),
309 { status: 401 },
310 );
311 }
312 },
313 },
314
315 "/api/counter/increment": {
316 POST: async (req) => {
317 try {
318 const userId = await requireAuth(req);
319 const count = incrementCounter(userId);
320
321 return new Response(JSON.stringify({ count }), {
322 headers: { "Content-Type": "application/json" },
323 });
324 } catch (error) {
325 return new Response(
326 JSON.stringify({
327 error:
328 error instanceof Error ? error.message : "Not authenticated",
329 }),
330 { status: 401 },
331 );
332 }
333 },
334 },
335
336 "/api/counter/decrement": {
337 POST: async (req) => {
338 try {
339 const userId = await requireAuth(req);
340 const count = decrementCounter(userId);
341
342 return new Response(JSON.stringify({ count }), {
343 headers: { "Content-Type": "application/json" },
344 });
345 } catch (error) {
346 return new Response(
347 JSON.stringify({
348 error:
349 error instanceof Error ? error.message : "Not authenticated",
350 }),
351 { status: 401 },
352 );
353 }
354 },
355 },
356
357 "/api/counter/reset": {
358 POST: async (req) => {
359 try {
360 const userId = await requireAuth(req);
361 resetCounter(userId);
362
363 return new Response(JSON.stringify({ count: 0 }), {
364 headers: { "Content-Type": "application/json" },
365 });
366 } catch (error) {
367 return new Response(
368 JSON.stringify({
369 error:
370 error instanceof Error ? error.message : "Not authenticated",
371 }),
372 { status: 401 },
373 );
374 }
375 },
376 },
377 },
378 development: {
379 hmr: true,
380 console: true,
381 },
382});
383
384console.log(`🥞 Tacy Stack running at http://localhost:${port}`);