- Frontend: Vite v8 + React v19 + Mantine v8
- Hosting: Cloudflare Pages
- Backend: Cloudflare Workers
- Database: Supabase (PostgreSQL)
- Initialize root workspace
npm init -y
- Replace
package.json
{
"name": "timekeeper",
"private": true,
"workspaces": [
"frontend",
"worker"
]
}
- Initialize git
Really needed?
git init
echo "node_modules" >> .gitignore
echo ".env.local" >> .gitignore
echo "dist" >> .gitignore
- Scaffold the front-end
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
- Install front-end packages
npm install @mantine/core@8 @mantine/hooks@8 @mantine/dates@8 @mantine/notifications@8
npm install postcss postcss-preset-mantine postcss-simple-vars --save-dev
npm install @tabler/icons-react
- Scaffold the back-end
cd ..
npm create cloudflare@latest -- worker
cd worker
- Install back-end packages
npm install resend
npm install @supabase/supabase-js
- Create
frontend/postcss.config.cjs
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
- Replace
frontend/src/main.tsx
Wire Mantine into app
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import { MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<MantineProvider>
<Notifications />
<App />
</MantineProvider>
</StrictMode>
);
- Replace
frontend/src/App.tsx(TBD)
Temporary page for smoke test
import { Button, Stack, Text } from '@mantine/core';
export default function App() {
return (
<Stack align="center" mt="xl">
<Text size="xl" fw={700}>Timekeeper ⏱</Text>
<Button>Clock In</Button>
</Stack>
);
}
-
Create
frontend/public/manifest.webmanifest{ "name": "Timekeeper", "short_name": "Timekeeper", "description": "Employee clock-in and shift management", "start_url": "/", "display": "standalone", "theme_color": "#228be6", "background_color": "#ffffff", "icons": [ { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } ] } -
Create
frontend/public/sw.js
Minimal service worker
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => self.clients.claim());
- Add below to
frontend/index.htmlinside<head>
Link manifest
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#228be6" />
- Add below to
frontend/src/main.tsxbeforecreateRoot()
Register the service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
- Create
frontend/src/lib/api.ts
Create Worker API client
const WORKER_URL = import.meta.env.VITE_WORKER_URL as string;
async function request<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const res = await fetch(`${WORKER_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(error.error || `HTTP ${res.status}`);
}
return res.json();
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) =>
request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
put: <T>(path: string, body: unknown) =>
request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
};
- Create
frontend/.env.local
VITE_WORKER_URL=http://localhost:8787
- Create
worker/.dev.vars(TBD)
Create Worker env file
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
RESEND_API_KEY=your_resend_api_key_here
- Create
worker/src/types/env.d.ts
Define the Worker environment types
interface Env {
SUPABASE_URL: string;
SUPABASE_SERVICE_ROLE_KEY: string;
RESEND_API_KEY: string;
}
- Create
worker/src/lib/supabase.ts
Create the Supabase client for the Worker
import { createClient } from '@supabase/supabase-js';
export function createSupabaseClient(env: Env) {
return createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
}
- Replace
worker/src/index.ts
Setup a basic Worker router
import { createSupabaseClient } from './lib/supabase';
export default {
// Handles HTTP requests from the frontend
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const supabase = createSupabaseClient(env);
// CORS headers — needed so the frontend can call the Worker
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
// Handle preflight
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// Health check
if (url.pathname === '/api/health') {
return Response.json({ status: 'ok' }, { headers: corsHeaders });
}
return Response.json(
{ error: 'Not found' },
{ status: 404, headers: corsHeaders }
);
},
// Handles scheduled cron jobs (pay report emails)
async scheduled(event: ScheduledEvent, env: Env): Promise<void> {
const supabase = createSupabaseClient(env);
console.log('Cron triggered:', event.cron);
// Pay report logic will go here later
},
};
- Add below to
worker/wrangler.jsoncbelow$schema(TBD)
Temporary scheduler
"triggers": {
"crons": [
"0 9 * * 5" // Every Friday at 9am UTC — pay report day
]
},
- Go to supabase.com -> "New Project"
- Fill in the details (no need to check anything)
- Go to "Setting" -> "API Keys"
- Copy the "Secret" key that starts with
sb_secret_... - Paste the secret key into
worker/.dev.varsforSUPABASE_SERVICE_ROLE_KEY
- Click on "Connect" at the top of the page
- Copy the "Project URL" that ends with
...supabase.co - Paste the URL into
worker/.dev.varsforSUPABASE_URL
- Go to resend.com -> "API Keys"
- "Create API Key" and name it
timekeeper-dev - Set permission to "Sending access" (since it's dev)
- Click "Add" and copy the key
- Paste the key into
worker/.dev.varsforRESEND_API_KEY
- Go to dash.cloudflare.com and click "Workers & Pages"
- Click "Create" -> "Connect to Git"
- Select your Github repo and begin setup:
- Root directory: frontend
- Build command: npm run build
- Build output directory: dist
- Click "Save and Deploy"
Wrangler deploys directly by reading
worker/wrangler.jsonc
- Deploy Worker via terminal
cd worker
npm wrangler deploy
- When done, copy the Worker URL that ends with
...workers.dev - Copy the URL and create environment variable for Pages project
- VITE_WORKER_URL: your_worker_url
- Add environment variables to Worker via terminal
npx wrangler secret put SUPABASE_URL
npx wrangler secret put SUPABASE_SERVICE_ROLE_KEY
npx wrangler secret put RESEND_API_KEY
- Redeploy Pages