create-workerstack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js'
2
+ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
3
+ import type { Logger as DrizzleLogger } from 'drizzle-orm/logger'
4
+ import type { Context } from 'hono'
5
+ import postgres from 'postgres'
6
+ import { Logger } from '@/utils/logger'
7
+ import * as schema from './schema'
8
+
9
+ /**
10
+ * Database logger adapter that bridges Drizzle ORM logging with our structured logger
11
+ *
12
+ * Implements Drizzle's Logger interface and formats SQL queries for
13
+ * structured logging in Cloudflare Workers.
14
+ */
15
+ class DatabaseLogger implements DrizzleLogger {
16
+ private logger: Logger
17
+
18
+ constructor(logger: Logger) {
19
+ this.logger = logger
20
+ }
21
+
22
+ /**
23
+ * Logs SQL queries executed by Drizzle ORM
24
+ *
25
+ * @param query - The SQL query string
26
+ * @param params - Query parameters
27
+ */
28
+ logQuery(query: string, params: unknown[]): void {
29
+ this.logger.debug('Database query executed', {
30
+ query,
31
+ params,
32
+ paramsCount: params.length,
33
+ })
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Initialize Drizzle ORM with PostgreSQL via postgres.js
39
+ *
40
+ * This function creates a Drizzle instance with:
41
+ * - Automatic camelCase → snake_case column mapping
42
+ * - Full schema awareness for type-safe queries
43
+ * - PostgreSQL connection via postgres.js
44
+ * - Optional structured query logging
45
+ * - Prefetch disabled for Transaction pool mode compatibility (e.g. Supabase, PgBouncer)
46
+ *
47
+ * @param connectionString - PostgreSQL connection URL
48
+ * @param logger - Optional Logger instance for query logging
49
+ * @returns Drizzle database instance
50
+ *
51
+ * @example
52
+ * // Without logging
53
+ * const db = initDB(env.DATABASE_URL)
54
+ *
55
+ * // With logging
56
+ * const logger = createLogger({ context: 'database' })
57
+ * const db = initDB(env.DATABASE_URL, logger)
58
+ * const rows = await db.select().from(schema.exampleTable)
59
+ */
60
+ export function initDB(
61
+ connectionString: string,
62
+ logger?: Logger
63
+ ): PostgresJsDatabase<typeof schema> {
64
+ const client = postgres(connectionString, { prepare: false })
65
+ return drizzle(client, {
66
+ schema,
67
+ casing: 'snake_case',
68
+ logger: logger ? new DatabaseLogger(logger) : undefined,
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Type export for use in Hono context and other places
74
+ */
75
+ export type Database = ReturnType<typeof initDB>
76
+
77
+ /**
78
+ * Hono middleware that initializes database and adds it to context
79
+ *
80
+ * Automatically initializes Drizzle with PostgreSQL and
81
+ * attaches the database instance to the context. If a logger is available
82
+ * in the context, it will be used for query logging.
83
+ *
84
+ * Expects DATABASE_URL to be set in the environment bindings.
85
+ *
86
+ * @example
87
+ * app.use('*', loggerMiddleware()) // Set up logger first
88
+ * app.use('*', dbMiddleware()) // Then set up database with logging
89
+ *
90
+ * // Using raw Drizzle queries
91
+ * app.get('/api/example', async (c) => {
92
+ * const rows = await c.var.db.select().from(schema.exampleTable)
93
+ * return c.json(rows)
94
+ * })
95
+ *
96
+ */
97
+ export function dbMiddleware() {
98
+ return async (c: Context, next: () => Promise<void>) => {
99
+ const logger = c.var.logger?.withContext({ component: 'database' })
100
+ const db = initDB(c.env.DATABASE_URL, logger)
101
+ c.set('db', db)
102
+ await next()
103
+ }
104
+ }
105
+
106
+ // Extend Hono's context type to include database
107
+ declare module 'hono' {
108
+ interface ContextVariableMap {
109
+ db: Database
110
+ }
111
+ }
@@ -0,0 +1 @@
1
+ export * from './auth-schema'
@@ -0,0 +1,17 @@
1
+ import { Hono } from 'hono'
2
+ import { renderer } from './client'
3
+ import { loggerMiddleware } from './utils/logger'
4
+ import { dbMiddleware } from './database/db'
5
+ import api from './api'
6
+
7
+ const app = new Hono()
8
+
9
+ app.use('/api/*', loggerMiddleware())
10
+ app.use('/api/*', dbMiddleware())
11
+ app.use(renderer)
12
+ app.route('/api', api)
13
+ app.get('*', (c) => {
14
+ return c.render(<div id="root"></div>)
15
+ })
16
+
17
+ export default app
@@ -0,0 +1,28 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { betterAuthOptions } from './options';
4
+ import type { Database } from "@/database/db";
5
+
6
+ import * as schema from "@/database/schema";
7
+
8
+ /**
9
+ * Better Auth Instance
10
+ */
11
+ export const auth = (db: Database, env: CloudflareBindings) => {
12
+ return betterAuth({
13
+ ...betterAuthOptions(env),
14
+ database: drizzleAdapter(db, {
15
+ provider: "pg",
16
+ schema,
17
+ }),
18
+ baseURL: env.BETTER_AUTH_URL!,
19
+ secret: env.BETTER_AUTH_SECRET!,
20
+ });
21
+ };
22
+
23
+ type SessionResult = NonNullable<Awaited<ReturnType<ReturnType<typeof auth>["api"]["getSession"]>>>;
24
+
25
+ export type AuthType = {
26
+ user: SessionResult["user"] | null
27
+ session: SessionResult["session"] | null
28
+ }
@@ -0,0 +1,29 @@
1
+ import { createEmailService } from "@/services/email";
2
+ import { BetterAuthOptions } from 'better-auth';
3
+ import { admin } from "better-auth/plugins";
4
+
5
+ export const betterAuthOptions = (env: CloudflareBindings): BetterAuthOptions => {
6
+ const emailService = createEmailService(env.RESEND_API_KEY);
7
+
8
+ return {
9
+ appName: 'YOUR_APP_NAME',
10
+ basePath: '/api/auth',
11
+
12
+ emailAndPassword: {
13
+ enabled: true,
14
+ requireEmailVerification: false,
15
+ sendResetPassword: async ({ user, url }) => {
16
+ await emailService.sendPasswordReset(user.email, url);
17
+ },
18
+ },
19
+ emailVerification: {
20
+ sendOnSignUp: true,
21
+ sendVerificationEmail: async ({ user, url }) => {
22
+ await emailService.sendVerificationEmail(user.email, url);
23
+ },
24
+ },
25
+ plugins: [
26
+ admin(),
27
+ ],
28
+ };
29
+ };
@@ -0,0 +1,51 @@
1
+ import { Resend } from "resend";
2
+
3
+ // TODO: Update with your app name and email domain
4
+ const FROM_EMAIL = "App <noreply@example.com>";
5
+
6
+ export function createEmailService(apiKey: string) {
7
+ const resend = new Resend(apiKey);
8
+
9
+ return {
10
+ async sendPasswordReset(to: string, url: string) {
11
+ await resend.emails.send({
12
+ from: FROM_EMAIL,
13
+ to,
14
+ subject: "Reset your password",
15
+ html: `
16
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
17
+ <h2 style="color: #1a1a2e;">Reset your password</h2>
18
+ <p>You requested a password reset. Click the button below to create a new password:</p>
19
+ <a href="${url}" style="display: inline-block; background-color: #7c3aed; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; margin: 16px 0;">
20
+ Reset Password
21
+ </a>
22
+ <p style="color: #666; font-size: 14px;">If you didn't request this change, you can safely ignore this email.</p>
23
+ <p style="color: #666; font-size: 14px;">This link expires in 1 hour.</p>
24
+ <hr style="border: none; border-top: 1px solid #eee; margin: 24px 0;" />
25
+ <p style="color: #999; font-size: 12px;">Sent by your app</p>
26
+ </div>
27
+ `,
28
+ });
29
+ },
30
+
31
+ async sendVerificationEmail(to: string, url: string) {
32
+ await resend.emails.send({
33
+ from: FROM_EMAIL,
34
+ to,
35
+ subject: "Verify your email",
36
+ html: `
37
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
38
+ <h2 style="color: #1a1a2e;">Verify your email</h2>
39
+ <p>Thanks for signing up! Click the button below to verify your email address:</p>
40
+ <a href="${url}" style="display: inline-block; background-color: #7c3aed; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; margin: 16px 0;">
41
+ Verify Email
42
+ </a>
43
+ <p style="color: #666; font-size: 14px;">If you didn't create an account, you can safely ignore this email.</p>
44
+ <hr style="border: none; border-top: 1px solid #eee; margin: 24px 0;" />
45
+ <p style="color: #999; font-size: 12px;">Sent by your app</p>
46
+ </div>
47
+ `,
48
+ });
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,45 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ html {
10
+ scroll-behavior: smooth;
11
+ }
12
+
13
+ body {
14
+ background-color: #0A0A0F;
15
+ color: #F1F5F9;
16
+ -webkit-font-smoothing: antialiased;
17
+ -moz-osx-font-smoothing: grayscale;
18
+ overflow-x: hidden;
19
+ }
20
+
21
+ a {
22
+ text-decoration: none;
23
+ color: inherit;
24
+ }
25
+
26
+ @keyframes float {
27
+ 0%, 100% { transform: translateY(0px); }
28
+ 50% { transform: translateY(-12px); }
29
+ }
30
+
31
+ @keyframes fadeInUp {
32
+ from {
33
+ opacity: 0;
34
+ transform: translateY(30px);
35
+ }
36
+ to {
37
+ opacity: 1;
38
+ transform: translateY(0);
39
+ }
40
+ }
41
+
42
+ @keyframes pulse {
43
+ 0%, 100% { opacity: 0.4; }
44
+ 50% { opacity: 0.8; }
45
+ }
@@ -0,0 +1,211 @@
1
+ import type { Context } from 'hono'
2
+
3
+ /**
4
+ * Log levels following standard severity
5
+ */
6
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
7
+
8
+ /**
9
+ * Structured log entry compatible with Cloudflare Workers Logs
10
+ * All fields are automatically indexed for querying
11
+ */
12
+ export interface LogEntry {
13
+ timestamp: string
14
+ level: LogLevel
15
+ message: string
16
+ requestId?: string
17
+ traceId?: string
18
+ [key: string]: unknown
19
+ }
20
+
21
+ /**
22
+ * Logger configuration options
23
+ */
24
+ export interface LoggerConfig {
25
+ minLevel?: LogLevel
26
+ context?: Record<string, unknown>
27
+ }
28
+
29
+ const LOG_LEVELS: Record<LogLevel, number> = {
30
+ debug: 0,
31
+ info: 1,
32
+ warn: 2,
33
+ error: 3,
34
+ }
35
+
36
+ /**
37
+ * Structured JSON logger for Cloudflare Workers
38
+ *
39
+ * Outputs logs in JSON format for automatic indexing by Workers Logs.
40
+ * Supports contextual metadata, request correlation, and flexible log levels.
41
+ *
42
+ * @example
43
+ * const logger = createLogger({ requestId: 'abc-123' })
44
+ * logger.info('User logged in', { userId: '456' })
45
+ * // Output: {"timestamp":"2025-12-08T...","level":"info","message":"User logged in","requestId":"abc-123","userId":"456"}
46
+ */
47
+ export class Logger {
48
+ private context: Record<string, unknown>
49
+ private minLevel: number
50
+
51
+ constructor(config: LoggerConfig = {}) {
52
+ this.context = config.context || {}
53
+ this.minLevel = LOG_LEVELS[config.minLevel || 'info']
54
+ }
55
+
56
+ /**
57
+ * Creates a child logger with additional context
58
+ * The new context is merged with the parent context
59
+ */
60
+ withContext(additionalContext: Record<string, unknown>): Logger {
61
+ return new Logger({
62
+ minLevel: this.getMinLevelName(),
63
+ context: { ...this.context, ...additionalContext },
64
+ })
65
+ }
66
+
67
+ /**
68
+ * Logs a message at the specified level
69
+ */
70
+ log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
71
+ if (LOG_LEVELS[level] < this.minLevel) {
72
+ return
73
+ }
74
+
75
+ const entry: LogEntry = {
76
+ timestamp: new Date().toISOString(),
77
+ level,
78
+ message,
79
+ ...this.context,
80
+ ...context,
81
+ }
82
+
83
+ // Truncate if exceeds Cloudflare's 256 KB limit
84
+ const logString = JSON.stringify(entry)
85
+ if (logString.length > 256000) {
86
+ console.log(
87
+ JSON.stringify({
88
+ ...entry,
89
+ message: entry.message.substring(0, 1000) + '... [truncated]',
90
+ _truncated: true,
91
+ _originalSize: logString.length,
92
+ })
93
+ )
94
+ } else {
95
+ console.log(logString)
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Log debug message (lowest priority)
101
+ */
102
+ debug(message: string, context?: Record<string, unknown>): void {
103
+ this.log('debug', message, context)
104
+ }
105
+
106
+ /**
107
+ * Log info message (normal priority)
108
+ */
109
+ info(message: string, context?: Record<string, unknown>): void {
110
+ this.log('info', message, context)
111
+ }
112
+
113
+ /**
114
+ * Log warning message (elevated priority)
115
+ */
116
+ warn(message: string, context?: Record<string, unknown>): void {
117
+ this.log('warn', message, context)
118
+ }
119
+
120
+ /**
121
+ * Log error message (highest priority)
122
+ * Automatically extracts stack trace from Error objects
123
+ */
124
+ error(message: string, context?: Record<string, unknown>): void {
125
+ const errorContext = { ...context }
126
+
127
+ // Extract error details if an Error object is provided
128
+ if (context?.error instanceof Error) {
129
+ errorContext.errorName = context.error.name
130
+ errorContext.errorMessage = context.error.message
131
+ errorContext.errorStack = context.error.stack
132
+ delete errorContext.error
133
+ }
134
+
135
+ this.log('error', message, errorContext)
136
+ }
137
+
138
+ private getMinLevelName(): LogLevel {
139
+ const entries = Object.entries(LOG_LEVELS)
140
+ const found = entries.find(([, value]) => value === this.minLevel)
141
+ return (found?.[0] as LogLevel) || 'info'
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Creates a new logger instance with optional context
147
+ */
148
+ export function createLogger(context?: Record<string, unknown>, minLevel: LogLevel = 'info'): Logger {
149
+ return new Logger({ context, minLevel })
150
+ }
151
+
152
+ /**
153
+ * Hono middleware that adds logger to context and logs requests
154
+ *
155
+ * Automatically logs request start/end with timing and status code.
156
+ * Attaches logger instance to c.var.logger for use in handlers.
157
+ *
158
+ * @example
159
+ * app.use('*', loggerMiddleware())
160
+ *
161
+ * app.get('/api/users', (c) => {
162
+ * c.var.logger.info('Fetching users')
163
+ * return c.json({ users: [] })
164
+ * })
165
+ */
166
+ export function loggerMiddleware() {
167
+ return async (c: Context, next: () => Promise<void>) => {
168
+ const requestId = crypto.randomUUID()
169
+ // Use performance.now() for measuring operation duration
170
+ // (more idiomatic than Date.now() for performance timing)
171
+ const startTime = performance.now()
172
+
173
+ // Create logger with request context
174
+ const logger = createLogger({
175
+ requestId,
176
+ method: c.req.method,
177
+ path: c.req.path,
178
+ userAgent: c.req.header('user-agent'),
179
+ ip: c.req.header('cf-connecting-ip'),
180
+ }, 'debug')
181
+
182
+ // Attach logger to context
183
+ c.set('logger', logger)
184
+
185
+ logger.info('Request started')
186
+
187
+ try {
188
+ await next()
189
+
190
+ const duration = performance.now() - startTime
191
+ logger.info('Request completed', {
192
+ status: c.res.status,
193
+ durationMs: duration,
194
+ })
195
+ } catch (error) {
196
+ const duration = performance.now() - startTime
197
+ logger.error('Request failed', {
198
+ error,
199
+ durationMs: duration,
200
+ })
201
+ throw error
202
+ }
203
+ }
204
+ }
205
+
206
+ // Extend Hono's context type to include logger
207
+ declare module 'hono' {
208
+ interface ContextVariableMap {
209
+ logger: Logger
210
+ }
211
+ }
@@ -0,0 +1,20 @@
1
+ import { MiddlewareHandler } from 'hono'
2
+
3
+ /**
4
+ * Middleware to disable Cloudflare caching for API routes
5
+ *
6
+ * Sets appropriate headers to prevent caching at CDN and browser levels:
7
+ * - Cache-Control: Prevents caching by proxies and browsers
8
+ * - Pragma: HTTP/1.0 backward compatibility
9
+ * - Expires: Sets immediate expiration
10
+ */
11
+ export const noCacheMiddleware = (): MiddlewareHandler => {
12
+ return async (c, next) => {
13
+ await next()
14
+
15
+ // Prevent caching by Cloudflare and browsers
16
+ c.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
17
+ c.header('Pragma', 'no-cache')
18
+ c.header('Expires', '0')
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
9
+ "types": ["vite/client", "./worker-configuration.d.ts", "bun-types"],
10
+ "jsx": "react-jsx",
11
+ "jsxImportSource": "hono/jsx",
12
+ "baseUrl": ".",
13
+ "paths": {
14
+ "@/*": ["src/*"]
15
+ }
16
+ },
17
+ "exclude": ["src/client", "src/client.tsx"]
18
+ }
@@ -0,0 +1,34 @@
1
+ import { cloudflare } from '@cloudflare/vite-plugin'
2
+ import { defineConfig } from 'vite'
3
+ import ssrPlugin from 'vite-ssr-components/plugin'
4
+ import react from '@vitejs/plugin-react'
5
+
6
+ import path from 'path'
7
+
8
+ export default defineConfig({
9
+ server: {
10
+ port: 5000,
11
+ host: '0.0.0.0',
12
+ allowedHosts: true,
13
+ watch: {
14
+ ignored: ['**/.cache/**', '**/node_modules/**'],
15
+ },
16
+ },
17
+ cacheDir: '/tmp/vite-cache',
18
+ resolve: {
19
+ alias: {
20
+ '@': path.resolve(__dirname, './src')
21
+ }
22
+ },
23
+ plugins: [
24
+ cloudflare(),
25
+ ssrPlugin({
26
+ entry: {
27
+ target: 'src/client.tsx',
28
+ },
29
+ }),
30
+ react({
31
+ include: /src\/client\/.*\.tsx?$/,
32
+ }),
33
+ ],
34
+ })