Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29ff3265e8 | |||
| b1d0267bf1 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Clearity is a workspace, client, and contract management platform for managed office operations. It tracks clients, contacts, addresses, rooms, services, bookings, contracts, invoices, and workspace admins from a SvelteKit dashboard.
|
Clearity is a workspace, client, and contract management platform for managed office operations. It tracks clients, contacts, addresses, rooms, services, bookings, contracts, invoices, and workspace admins from a SvelteKit dashboard.
|
||||||
|
|
||||||
The app is built with SvelteKit, Svelte 5, shadcn-svelte, Superforms, Better Auth, Drizzle ORM, SQLite/libSQL, and Cloudflare Workers.
|
The app is built with SvelteKit, Svelte 5, shadcn-svelte, Superforms, Better Auth, Drizzle ORM, SQLite/libSQL for local development, Cloudflare D1 for production, and Cloudflare Workers.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -52,19 +52,26 @@ pnpm db:studio # Open Drizzle Studio
|
|||||||
pnpm gen # Regenerate Cloudflare Worker types
|
pnpm gen # Regenerate Cloudflare Worker types
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables And Bindings
|
||||||
|
|
||||||
Required:
|
Required:
|
||||||
|
|
||||||
- `DATABASE_URL`: SQLite/libSQL connection URL. Use `file:local.db` for local development.
|
- `DATABASE_URL`: SQLite/libSQL connection URL for local development. Use `file:local.db`.
|
||||||
- `ORIGIN`: Public app origin, for example `http://localhost:5173` locally or `https://clearity.example.com` in production.
|
- `ORIGIN`: Public app origin, for example `http://localhost:5173` locally or `https://clearity.example.com` in production.
|
||||||
- `BETTER_AUTH_SECRET`: Secret used by Better Auth.
|
- `BETTER_AUTH_SECRET`: Secret used by Better Auth.
|
||||||
|
|
||||||
|
Production also requires the Cloudflare D1 binding named `DB`, configured in `wrangler.jsonc`. Do not set `DATABASE_URL` in production unless you intentionally want to use a hosted libSQL database instead of D1.
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
The application schema lives in `src/lib/server/db`. Drizzle migration files live in `drizzle`.
|
The application schema lives in `src/lib/server/db`. Drizzle migration files live in `drizzle`.
|
||||||
|
|
||||||
Local development currently uses the libSQL client through `DATABASE_URL`, so the fastest local database is a SQLite file:
|
Runtime database selection is:
|
||||||
|
|
||||||
|
- If `DATABASE_URL` is set, Clearity uses the libSQL driver.
|
||||||
|
- If `DATABASE_URL` is not set and the Cloudflare `DB` binding exists, Clearity uses Drizzle's Cloudflare D1 driver.
|
||||||
|
|
||||||
|
The fastest local database is a SQLite file:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
DATABASE_URL=file:local.db pnpm db:migrate
|
DATABASE_URL=file:local.db pnpm db:migrate
|
||||||
@@ -77,6 +84,14 @@ pnpm db:generate
|
|||||||
DATABASE_URL=file:local.db pnpm db:migrate
|
DATABASE_URL=file:local.db pnpm db:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To test against Wrangler's local D1 simulator instead, leave `DATABASE_URL` unset, apply migrations locally, and run the app through the Cloudflare Worker build:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm build
|
||||||
|
pnpm wrangler d1 migrations apply clearity-production --local
|
||||||
|
pnpm wrangler dev
|
||||||
|
```
|
||||||
|
|
||||||
## Production Deployment
|
## Production Deployment
|
||||||
|
|
||||||
The Worker is configured in `wrangler.jsonc` and built with the SvelteKit Cloudflare adapter.
|
The Worker is configured in `wrangler.jsonc` and built with the SvelteKit Cloudflare adapter.
|
||||||
@@ -91,10 +106,10 @@ pnpm wrangler whoami
|
|||||||
Create a production D1 database:
|
Create a production D1 database:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm wrangler d1 create clearity-production
|
pnpm wrangler d1 create clearity-production --binding DB
|
||||||
```
|
```
|
||||||
|
|
||||||
Wrangler prints a `database_id`. Add it to `wrangler.jsonc`:
|
Wrangler prints a `database_id`. Replace the placeholder in `wrangler.jsonc`:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
@@ -102,7 +117,8 @@ Wrangler prints a `database_id`. Add it to `wrangler.jsonc`:
|
|||||||
{
|
{
|
||||||
"binding": "DB",
|
"binding": "DB",
|
||||||
"database_name": "clearity-production",
|
"database_name": "clearity-production",
|
||||||
"database_id": "<database_id>"
|
"database_id": "<database_id>",
|
||||||
|
"migrations_dir": "drizzle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -117,9 +133,7 @@ pnpm gen
|
|||||||
Apply SQL migrations to the remote D1 database:
|
Apply SQL migrations to the remote D1 database:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
for file in drizzle/*.sql; do
|
pnpm wrangler d1 migrations apply clearity-production --remote
|
||||||
pnpm wrangler d1 execute clearity-production --remote --file "$file"
|
|
||||||
done
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Set production secrets:
|
Set production secrets:
|
||||||
@@ -129,11 +143,7 @@ pnpm wrangler secret put ORIGIN
|
|||||||
pnpm wrangler secret put BETTER_AUTH_SECRET
|
pnpm wrangler secret put BETTER_AUTH_SECRET
|
||||||
```
|
```
|
||||||
|
|
||||||
This codebase currently reads the database through `DATABASE_URL`. If production is deployed against Cloudflare D1, wire the runtime database client to the `DB` D1 binding before deploying. If production is deployed against a hosted libSQL database instead, set `DATABASE_URL` as a Worker secret:
|
Production uses the `DB` binding by default. `DATABASE_URL` is only needed as a production secret if you intentionally deploy against a hosted libSQL database instead of D1.
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm wrangler secret put DATABASE_URL
|
|
||||||
```
|
|
||||||
|
|
||||||
Build and deploy:
|
Build and deploy:
|
||||||
|
|
||||||
|
|||||||
+14
-3
@@ -3,13 +3,24 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|||||||
import { admin } from 'better-auth/plugins';
|
import { admin } from 'better-auth/plugins';
|
||||||
import { sveltekitCookies } from 'better-auth/svelte-kit';
|
import { sveltekitCookies } from 'better-auth/svelte-kit';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { building } from '$app/environment';
|
||||||
import { getRequestEvent } from '$app/server';
|
import { getRequestEvent } from '$app/server';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
|
import * as schema from '$lib/server/db/schema';
|
||||||
|
|
||||||
|
const buildTimeOrigin = 'http://localhost:5173';
|
||||||
|
const buildTimeSecret = 'clearity-build-time-placeholder-secret';
|
||||||
|
|
||||||
|
const origin = env.ORIGIN ?? (building ? buildTimeOrigin : undefined);
|
||||||
|
const secret = env.BETTER_AUTH_SECRET ?? (building ? buildTimeSecret : undefined);
|
||||||
|
|
||||||
|
if (!origin) throw new Error('ORIGIN is not set');
|
||||||
|
if (!secret) throw new Error('BETTER_AUTH_SECRET is not set');
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
baseURL: env.ORIGIN,
|
baseURL: origin,
|
||||||
secret: env.BETTER_AUTH_SECRET,
|
secret,
|
||||||
database: drizzleAdapter(db, { provider: 'sqlite' }),
|
database: drizzleAdapter(db, { provider: 'sqlite', schema }),
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
disableSignUp: true
|
disableSignUp: true
|
||||||
|
|||||||
@@ -1,10 +1,74 @@
|
|||||||
import { drizzle } from 'drizzle-orm/libsql';
|
|
||||||
import { createClient } from '@libsql/client';
|
|
||||||
import * as schema from './schema';
|
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { getRequestEvent } from '$app/server';
|
||||||
|
import { createClient, type Client } from '@libsql/client';
|
||||||
|
import { drizzle as drizzleD1, type DrizzleD1Database } from 'drizzle-orm/d1';
|
||||||
|
import { drizzle as drizzleLibSQL, type LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||||
|
import * as schema from './schema';
|
||||||
|
|
||||||
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
type Schema = typeof schema;
|
||||||
|
type LocalDatabase = LibSQLDatabase<Schema> & { $client: Client };
|
||||||
|
type D1DatabaseClient = DrizzleD1Database<Schema> & { $client: D1Database };
|
||||||
|
|
||||||
const client = createClient({ url: env.DATABASE_URL });
|
export type Database = LocalDatabase;
|
||||||
|
export type RuntimeDatabase = LocalDatabase | D1DatabaseClient;
|
||||||
|
|
||||||
export const db = drizzle(client, { schema });
|
let localDb: LocalDatabase | undefined;
|
||||||
|
const d1Databases = new WeakMap<D1Database, D1DatabaseClient>();
|
||||||
|
|
||||||
|
function getD1Binding() {
|
||||||
|
try {
|
||||||
|
return getRequestEvent().platform?.env.DB;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalDb() {
|
||||||
|
if (localDb) return localDb;
|
||||||
|
if (!env.DATABASE_URL)
|
||||||
|
throw new Error('DATABASE_URL is not set and no Cloudflare D1 DB binding is available');
|
||||||
|
|
||||||
|
const client = createClient({ url: env.DATABASE_URL });
|
||||||
|
localDb = drizzleLibSQL(client, { schema });
|
||||||
|
|
||||||
|
return localDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getD1Db(binding: D1Database) {
|
||||||
|
const cached = d1Databases.get(binding);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const database = drizzleD1(binding, { schema });
|
||||||
|
d1Databases.set(binding, database);
|
||||||
|
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDb(): RuntimeDatabase {
|
||||||
|
if (env.DATABASE_URL) return getLocalDb();
|
||||||
|
|
||||||
|
const d1Binding = getD1Binding();
|
||||||
|
if (d1Binding) return getD1Db(d1Binding);
|
||||||
|
|
||||||
|
return getLocalDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = new Proxy({} as Database, {
|
||||||
|
get(_target, property) {
|
||||||
|
const database = getDb();
|
||||||
|
const value = Reflect.get(database, property);
|
||||||
|
return typeof value === 'function' ? value.bind(database) : value;
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(_target, property) {
|
||||||
|
return Reflect.getOwnPropertyDescriptor(getDb(), property);
|
||||||
|
},
|
||||||
|
getPrototypeOf() {
|
||||||
|
return Reflect.getPrototypeOf(getDb());
|
||||||
|
},
|
||||||
|
has(_target, property) {
|
||||||
|
return property in getDb();
|
||||||
|
},
|
||||||
|
ownKeys() {
|
||||||
|
return Reflect.ownKeys(getDb());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Vendored
+2
-4
@@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// Generated by Wrangler by running `wrangler types` (hash: 61c64c9cd1c2465ff3a92bc5ac82d57d)
|
// Generated by Wrangler by running `wrangler types` (hash: f6e835394237f01fba038726e2d4f9ae)
|
||||||
// Runtime types generated with workerd@1.20260521.1 2026-05-23 nodejs_als
|
// Runtime types generated with workerd@1.20260521.1 2026-05-23 nodejs_als
|
||||||
interface __BaseEnv_Env {
|
interface __BaseEnv_Env {
|
||||||
|
DB: D1Database;
|
||||||
ASSETS: Fetcher;
|
ASSETS: Fetcher;
|
||||||
DATABASE_URL: string;
|
|
||||||
ORIGIN: string;
|
|
||||||
BETTER_AUTH_SECRET: string;
|
|
||||||
}
|
}
|
||||||
declare namespace Cloudflare {
|
declare namespace Cloudflare {
|
||||||
interface Env extends __BaseEnv_Env {}
|
interface Env extends __BaseEnv_Env {}
|
||||||
|
|||||||
@@ -8,6 +8,14 @@
|
|||||||
"binding": "ASSETS",
|
"binding": "ASSETS",
|
||||||
"directory": ".svelte-kit/cloudflare"
|
"directory": ".svelte-kit/cloudflare"
|
||||||
},
|
},
|
||||||
|
"d1_databases": [
|
||||||
|
{
|
||||||
|
"binding": "DB",
|
||||||
|
"database_name": "clearity-production",
|
||||||
|
"database_id": "<replace-with-d1-database-id>",
|
||||||
|
"migrations_dir": "drizzle"
|
||||||
|
}
|
||||||
|
],
|
||||||
"workers_dev": true,
|
"workers_dev": true,
|
||||||
"preview_urls": true
|
"preview_urls": true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user