create-mikstack 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.
- package/README.md +54 -0
- package/dist/index.js +410 -0
- package/package.json +43 -0
- package/templates/adapters/cloudflare/package.json.partial +5 -0
- package/templates/adapters/cloudflare/svelte.config.js +19 -0
- package/templates/adapters/node/Dockerfile +30 -0
- package/templates/adapters/node/docker-compose.prod.yml +27 -0
- package/templates/adapters/node/package.json.partial +5 -0
- package/templates/adapters/node/svelte.config.js +19 -0
- package/templates/adapters/vercel/package.json.partial +5 -0
- package/templates/adapters/vercel/svelte.config.js +19 -0
- package/templates/base/.env.example +23 -0
- package/templates/base/.gitignore.append +2 -0
- package/templates/base/.mcp.json +9 -0
- package/templates/base/.prettierignore +10 -0
- package/templates/base/.vscode/extensions.json +3 -0
- package/templates/base/AGENTS.md +123 -0
- package/templates/base/README.md +27 -0
- package/templates/base/agents.md +28 -0
- package/templates/base/docker-compose.yml +15 -0
- package/templates/base/drizzle-zero.config.ts +17 -0
- package/templates/base/drizzle.config.ts +17 -0
- package/templates/base/eslint.config.ts +65 -0
- package/templates/base/package.json.partial +43 -0
- package/templates/base/prettier.config.js +6 -0
- package/templates/base/src/app.d.ts +12 -0
- package/templates/base/src/app.html +11 -0
- package/templates/base/src/hooks.server.ts +15 -0
- package/templates/base/src/lib/auth-client.ts +6 -0
- package/templates/base/src/lib/server/auth.ts +52 -0
- package/templates/base/src/lib/server/db/index.ts +19 -0
- package/templates/base/src/lib/server/db/schema.ts +117 -0
- package/templates/base/src/lib/server/db/seed.ts +21 -0
- package/templates/base/src/lib/server/emails/magic-link.ts +77 -0
- package/templates/base/src/lib/server/emails/send.ts +55 -0
- package/templates/base/src/lib/server/notifications/definitions.ts +12 -0
- package/templates/base/src/lib/server/notifications.ts +38 -0
- package/templates/base/src/lib/z.svelte.ts +14 -0
- package/templates/base/src/lib/zero/context.ts +9 -0
- package/templates/base/src/lib/zero/db-provider.server.ts +11 -0
- package/templates/base/src/lib/zero/mutators.ts +35 -0
- package/templates/base/src/lib/zero/queries.ts +21 -0
- package/templates/base/src/lib/zero/schema.ts +1 -0
- package/templates/base/src/routes/+layout.server.ts +5 -0
- package/templates/base/src/routes/+layout.svelte +7 -0
- package/templates/base/src/routes/+page.server.ts +7 -0
- package/templates/base/src/routes/+page.svelte +319 -0
- package/templates/base/src/routes/api/dev/emails/+server.ts +89 -0
- package/templates/base/src/routes/api/dev/emails/[id]/+server.ts +24 -0
- package/templates/base/src/routes/api/notifications/[...path]/+server.ts +10 -0
- package/templates/base/src/routes/api/zero/get-queries/+server.ts +29 -0
- package/templates/base/src/routes/api/zero/mutate/+server.ts +31 -0
- package/templates/base/src/routes/sign-in/+page.svelte +97 -0
- package/templates/base/tsconfig.json +40 -0
- package/templates/github-actions-bun/.github/workflows/ci.yml +22 -0
- package/templates/github-actions-npm/.github/workflows/ci.yml +25 -0
- package/templates/github-actions-pnpm/.github/workflows/ci.yml +27 -0
- package/templates/i18n/lingui.config.ts +16 -0
- package/templates/i18n/package.json.partial +14 -0
- package/templates/i18n/src/lib/i18n.ts +10 -0
- package/templates/i18n/src/locales/en.po +6 -0
- package/templates/i18n/src/po.d.ts +3 -0
- package/templates/i18n/vite.config.ts +7 -0
- package/templates/supply-chain-bun/bunfig.toml +3 -0
- package/templates/testing/package.json.partial +11 -0
- package/templates/testing/src/example.test.ts +7 -0
- package/templates/testing/src/lib/server/db/test-utils.ts +25 -0
- package/templates/testing/vitest.config.ts +9 -0
- package/templates/ui/.vscode/extensions.json +8 -0
- package/templates/ui/package.json.partial +13 -0
- package/templates/ui/src/app.css +94 -0
- package/templates/ui/src/routes/+layout.svelte +12 -0
- package/templates/ui/stylelint.config.js +7 -0
- package/templates/ui/vite.config.ts +6 -0
- package/templates/ui-dependency/package.json.partial +5 -0
- package/templates/ui-vendor/src/lib/components/ui/Accordion/Accordion.svelte +71 -0
- package/templates/ui-vendor/src/lib/components/ui/Accordion/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Alert/Alert.svelte +60 -0
- package/templates/ui-vendor/src/lib/components/ui/Alert/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Badge/Badge.svelte +48 -0
- package/templates/ui-vendor/src/lib/components/ui/Badge/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Button/Button.svelte +77 -0
- package/templates/ui-vendor/src/lib/components/ui/Button/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Card/Card.svelte +49 -0
- package/templates/ui-vendor/src/lib/components/ui/Card/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Dialog/Dialog.svelte +70 -0
- package/templates/ui-vendor/src/lib/components/ui/Dialog/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/FormField/FormField.svelte +53 -0
- package/templates/ui-vendor/src/lib/components/ui/FormField/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Input/Input.svelte +27 -0
- package/templates/ui-vendor/src/lib/components/ui/Input/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Separator/Separator.svelte +26 -0
- package/templates/ui-vendor/src/lib/components/ui/Separator/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Skeleton/Skeleton.svelte +40 -0
- package/templates/ui-vendor/src/lib/components/ui/Skeleton/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Switch/Switch.svelte +86 -0
- package/templates/ui-vendor/src/lib/components/ui/Switch/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Textarea/Textarea.svelte +29 -0
- package/templates/ui-vendor/src/lib/components/ui/Textarea/index.ts +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
SvelteKit app with Drizzle ORM, Zero (real-time sync), and better-auth.
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- **Framework**: SvelteKit with TypeScript
|
|
8
|
+
- **Database**: PostgreSQL via Drizzle ORM
|
|
9
|
+
- **Real-time**: Zero (@rocicorp/zero) for client-side sync
|
|
10
|
+
- **Auth**: better-auth with magic link
|
|
11
|
+
- **Styling**: See `src/app.css`
|
|
12
|
+
<!-- {{#if:i18n}} -->
|
|
13
|
+
- **i18n**: @mikstack/svelte-lingui (Lingui-based)
|
|
14
|
+
<!-- {{/if:i18n}} -->
|
|
15
|
+
|
|
16
|
+
## Key Patterns
|
|
17
|
+
|
|
18
|
+
### Zero Queries & Mutations
|
|
19
|
+
|
|
20
|
+
Queries: `src/lib/zero/queries.ts` — define with `defineQueries`
|
|
21
|
+
Mutators: `src/lib/zero/mutators.ts` — define with `defineMutators`
|
|
22
|
+
Client usage: `get_z()` from `$lib/z.svelte` returns the Zero instance
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
const z = get_z();
|
|
26
|
+
const query = z.q(queries.myQuery());
|
|
27
|
+
const data = $derived(query.data);
|
|
28
|
+
await z.mutate(mutators.myMutation(args));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Auth
|
|
32
|
+
|
|
33
|
+
Server: `src/lib/server/auth.ts` (better-auth config)
|
|
34
|
+
Client: `src/lib/auth-client.ts` (magic link + client helpers)
|
|
35
|
+
Session is available in `locals.user` and `locals.session` via `src/hooks.server.ts`.
|
|
36
|
+
|
|
37
|
+
### Forms (@mikstack/form)
|
|
38
|
+
|
|
39
|
+
Use `createForm` from `@mikstack/form` for client-side forms with Valibot validation.
|
|
40
|
+
Ideal for Zero mutators where forms don't use SvelteKit form actions.
|
|
41
|
+
|
|
42
|
+
```svelte
|
|
43
|
+
<script lang="ts">
|
|
44
|
+
import { createForm } from "@mikstack/form";
|
|
45
|
+
import * as v from "valibot";
|
|
46
|
+
|
|
47
|
+
const form = createForm({
|
|
48
|
+
schema: v.object({
|
|
49
|
+
title: v.pipe(v.string(), v.minLength(1, "Required")),
|
|
50
|
+
content: v.string(),
|
|
51
|
+
}),
|
|
52
|
+
initialValues: { title: "", content: "" },
|
|
53
|
+
async onSubmit(data) {
|
|
54
|
+
await z.mutate(mutators.note.create({ id: crypto.randomUUID(), ...data }));
|
|
55
|
+
form.reset();
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<form id={form.id} onsubmit={form.onsubmit}>
|
|
61
|
+
<input {...form.fields.title.as("text")} />
|
|
62
|
+
<button type="submit" disabled={form.pending}>Create</button>
|
|
63
|
+
</form>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Key APIs: `form.fields.<name>.as(type)` for input props, `.issues()` for errors,
|
|
67
|
+
`form.pending` / `form.error` / `form.result` for submit state, `form.reset()` to clear.
|
|
68
|
+
|
|
69
|
+
### Notifications (@mikstack/notifications)
|
|
70
|
+
|
|
71
|
+
Config: `src/lib/server/notifications.ts` (lazy-initialized via Proxy)
|
|
72
|
+
Definitions: `src/lib/server/notifications/definitions.ts`
|
|
73
|
+
API routes: `src/routes/api/notifications/` (mark-read, preferences)
|
|
74
|
+
|
|
75
|
+
Send a notification:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { notif } from "$lib/server/notifications";
|
|
79
|
+
await notif.send({ type: "welcome", userId: user.id, data: { userName: user.name } });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Define new notification types in `definitions.ts` using `defineNotification()`.
|
|
83
|
+
In-app notifications sync to the client via Zero (`inAppNotification` table).
|
|
84
|
+
Email delivery tracking and retries are handled automatically.
|
|
85
|
+
|
|
86
|
+
### Database
|
|
87
|
+
|
|
88
|
+
Schema: `src/lib/server/db/schema.ts`
|
|
89
|
+
Connection: `src/lib/server/db/index.ts` (lazy-initialized via Proxy)
|
|
90
|
+
|
|
91
|
+
<!-- {{#if:i18n}} -->
|
|
92
|
+
### i18n
|
|
93
|
+
|
|
94
|
+
Setup: `src/lib/i18n.ts` — initialized in root layout
|
|
95
|
+
Config: `lingui.config.ts`
|
|
96
|
+
Catalogs: `src/locales/{locale}.po`
|
|
97
|
+
|
|
98
|
+
Use `useLingui()` for translations, `<T>` component for rich text:
|
|
99
|
+
|
|
100
|
+
```svelte
|
|
101
|
+
<script lang="ts">
|
|
102
|
+
import { useLingui } from "@mikstack/svelte-lingui";
|
|
103
|
+
const { t } = useLingui();
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<h1>{t`Hello world`}</h1>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Run `{{pmRun}} i18n:extract` to extract messages into `.po` catalogs.
|
|
110
|
+
<!-- {{/if:i18n}} -->
|
|
111
|
+
|
|
112
|
+
## Commands
|
|
113
|
+
|
|
114
|
+
- `{{pmRun}} dev` — start dev server
|
|
115
|
+
- `{{pmRun}} build` — production build
|
|
116
|
+
- `{{pmRun}} db:push` — push schema to database
|
|
117
|
+
- `{{pmRun}} db:seed` — seed database
|
|
118
|
+
- `{{pmRun}} zero:generate` — generate Zero schema from Drizzle
|
|
119
|
+
- `{{pmRun}} lint` — run ESLint
|
|
120
|
+
- `{{pmRun}} format` — format with Prettier
|
|
121
|
+
<!-- {{#if:i18n}} -->
|
|
122
|
+
- `{{pmRun}} i18n:extract` — extract messages to .po catalogs
|
|
123
|
+
<!-- {{/if:i18n}} -->
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Built with [mikstack](https://github.com/mikaelsiidorow/mikstack).
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
{{pmRun}} db:start
|
|
9
|
+
{{pmRun}} db:push
|
|
10
|
+
{{pmRun}} db:seed
|
|
11
|
+
{{pmRun}} dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Scripts
|
|
15
|
+
|
|
16
|
+
- `{{pmRun}} dev` — Start dev server
|
|
17
|
+
- `{{pmRun}} build` — Build for production
|
|
18
|
+
- `{{pmRun}} preview` — Preview production build
|
|
19
|
+
- `{{pmRun}} check` — Run svelte-check
|
|
20
|
+
- `{{pmRun}} lint` — Lint with ESLint
|
|
21
|
+
- `{{pmRun}} format` — Format with Prettier
|
|
22
|
+
- `{{pmRun}} db:start` — Start Postgres (Docker)
|
|
23
|
+
- `{{pmRun}} db:generate` — Generate Drizzle migrations
|
|
24
|
+
- `{{pmRun}} db:migrate` — Run Drizzle migrations
|
|
25
|
+
- `{{pmRun}} db:push` — Push schema to database
|
|
26
|
+
- `{{pmRun}} db:studio` — Open Drizzle Studio
|
|
27
|
+
- `{{pmRun}} db:seed` — Seed the database
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Agent Guidelines for {{projectName}}
|
|
2
|
+
|
|
3
|
+
## Architecture Overview
|
|
4
|
+
|
|
5
|
+
This is a SvelteKit application using:
|
|
6
|
+
|
|
7
|
+
- **Drizzle ORM** for database schema and migrations
|
|
8
|
+
- **Zero** (@rocicorp/zero) for real-time client-side data sync
|
|
9
|
+
- **better-auth** for authentication (magic link flow)
|
|
10
|
+
|
|
11
|
+
## File Conventions
|
|
12
|
+
|
|
13
|
+
- Server-only code: `src/lib/server/` (enforced by SvelteKit)
|
|
14
|
+
- Zero schema/queries/mutators: `src/lib/zero/`
|
|
15
|
+
- Auth client: `src/lib/auth-client.ts`
|
|
16
|
+
- Zero client context: `src/lib/z.svelte.ts`
|
|
17
|
+
- API routes for Zero: `src/routes/api/zero/`
|
|
18
|
+
|
|
19
|
+
## When Modifying the Schema
|
|
20
|
+
|
|
21
|
+
1. Update Drizzle schema in `src/lib/server/db/schema.ts`
|
|
22
|
+
2. Run `{{pmRun}} db:push` (dev) or `{{pmRun}} db:generate` + `{{pmRun}} db:migrate` (production)
|
|
23
|
+
3. Run `{{pmRun}} zero:generate` to regenerate Zero schema
|
|
24
|
+
4. Update queries/mutators in `src/lib/zero/` as needed
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
Run `{{pmRun}} check` for type checking and `{{pmRun}} lint` for linting before committing.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
services:
|
|
2
|
+
db:
|
|
3
|
+
image: postgres
|
|
4
|
+
restart: always
|
|
5
|
+
ports:
|
|
6
|
+
- 5432:5432
|
|
7
|
+
environment:
|
|
8
|
+
POSTGRES_USER: root
|
|
9
|
+
POSTGRES_PASSWORD: mysecretpassword
|
|
10
|
+
POSTGRES_DB: local
|
|
11
|
+
command: ["postgres", "-c", "wal_level=logical"]
|
|
12
|
+
volumes:
|
|
13
|
+
- pgdata:/var/lib/postgresql/data
|
|
14
|
+
volumes:
|
|
15
|
+
pgdata:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { drizzleZeroConfig } from "drizzle-zero";
|
|
2
|
+
import * as schema from "./src/lib/server/db/schema";
|
|
3
|
+
|
|
4
|
+
export default drizzleZeroConfig(schema, {
|
|
5
|
+
tables: {
|
|
6
|
+
user: true,
|
|
7
|
+
note: true,
|
|
8
|
+
inAppNotification: true,
|
|
9
|
+
// Exclude auth-internal and server-only tables from client sync
|
|
10
|
+
session: false,
|
|
11
|
+
account: false,
|
|
12
|
+
verification: false,
|
|
13
|
+
notificationDelivery: false,
|
|
14
|
+
notificationPreference: false,
|
|
15
|
+
},
|
|
16
|
+
casing: "snake_case",
|
|
17
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
|
|
4
|
+
if (existsSync(".env")) {
|
|
5
|
+
process.loadEnvFile();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DATABASE_URL = process.env.DATABASE_URL || "postgresql://placeholder";
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
schema: "./src/lib/server/db/schema.ts",
|
|
12
|
+
dialect: "postgresql",
|
|
13
|
+
dbCredentials: { url: DATABASE_URL },
|
|
14
|
+
verbose: true,
|
|
15
|
+
strict: true,
|
|
16
|
+
casing: "snake_case",
|
|
17
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import prettier from "eslint-config-prettier";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { includeIgnoreFile } from "@eslint/compat";
|
|
4
|
+
import js from "@eslint/js";
|
|
5
|
+
import svelte from "eslint-plugin-svelte";
|
|
6
|
+
import { defineConfig } from "eslint/config";
|
|
7
|
+
import globals from "globals";
|
|
8
|
+
import ts from "typescript-eslint";
|
|
9
|
+
import svelteConfig from "./svelte.config.js";
|
|
10
|
+
|
|
11
|
+
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
|
|
12
|
+
|
|
13
|
+
const extraFileExtensions = [".svelte"];
|
|
14
|
+
|
|
15
|
+
export default defineConfig(
|
|
16
|
+
includeIgnoreFile(gitignorePath),
|
|
17
|
+
{ ignores: ["!*.gen.ts"] },
|
|
18
|
+
js.configs.recommended,
|
|
19
|
+
...ts.configs.recommendedTypeChecked,
|
|
20
|
+
...svelte.configs.recommended,
|
|
21
|
+
prettier,
|
|
22
|
+
...svelte.configs.prettier,
|
|
23
|
+
{
|
|
24
|
+
languageOptions: {
|
|
25
|
+
globals: { ...globals.browser, ...globals.node },
|
|
26
|
+
parserOptions: {
|
|
27
|
+
projectService: true,
|
|
28
|
+
extraFileExtensions,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
rules: {
|
|
32
|
+
"no-undef": "off",
|
|
33
|
+
"no-restricted-imports": [
|
|
34
|
+
"error",
|
|
35
|
+
{
|
|
36
|
+
paths: [
|
|
37
|
+
{
|
|
38
|
+
name: "$app/stores",
|
|
39
|
+
message: "Use $app/state instead of $app/stores.",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
|
|
48
|
+
languageOptions: {
|
|
49
|
+
parserOptions: {
|
|
50
|
+
projectService: true,
|
|
51
|
+
parser: ts.parser,
|
|
52
|
+
svelteConfig,
|
|
53
|
+
extraFileExtensions,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
rules: {
|
|
57
|
+
"@typescript-eslint/no-unsafe-argument": "off",
|
|
58
|
+
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
59
|
+
"@typescript-eslint/no-unsafe-call": "off",
|
|
60
|
+
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
61
|
+
"@typescript-eslint/no-unsafe-return": "off",
|
|
62
|
+
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"check": "drizzle-zero generate --output src/lib/zero/zero-schema.gen.ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
4
|
+
"check:watch": "drizzle-zero generate --output src/lib/zero/zero-schema.gen.ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
5
|
+
"lint": "drizzle-zero generate --output src/lib/zero/zero-schema.gen.ts && eslint .",
|
|
6
|
+
"format": "prettier --write .",
|
|
7
|
+
"format:check": "prettier --check .",
|
|
8
|
+
"db:start": "docker compose up -d",
|
|
9
|
+
"db:generate": "drizzle-kit generate",
|
|
10
|
+
"db:migrate": "drizzle-kit migrate",
|
|
11
|
+
"db:push": "drizzle-kit push",
|
|
12
|
+
"db:studio": "drizzle-kit studio",
|
|
13
|
+
"db:seed": "{{seedRunner}} src/lib/server/db/seed.ts",
|
|
14
|
+
"zero:generate": "drizzle-zero generate --output src/lib/zero/zero-schema.gen.ts",
|
|
15
|
+
"zero:start": "zero-cache-server"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@mikstack/email": "^0.1.0",
|
|
19
|
+
"@mikstack/form": "^0.1.0",
|
|
20
|
+
"@mikstack/notifications": "^0.1.0",
|
|
21
|
+
"@rocicorp/zero": "^0.25.12",
|
|
22
|
+
"better-auth": "^1.4.18",
|
|
23
|
+
"drizzle-orm": "^0.45.1",
|
|
24
|
+
"nodemailer": "^8.0.1",
|
|
25
|
+
"postgres": "^3.4.8",
|
|
26
|
+
"valibot": "^1.2.0",
|
|
27
|
+
"zero-svelte": "^1.2.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@eslint/compat": "^1.4.1",
|
|
31
|
+
"@eslint/js": "^9.39.2",
|
|
32
|
+
"@types/nodemailer": "^7.0.9",
|
|
33
|
+
"drizzle-kit": "^0.31.8",
|
|
34
|
+
"drizzle-zero": "^0.17.3",
|
|
35
|
+
"eslint": "^9.39.2",
|
|
36
|
+
"eslint-config-prettier": "^10.1.8",
|
|
37
|
+
"eslint-plugin-svelte": "^3.14.0",
|
|
38
|
+
"globals": "^17.3.0",
|
|
39
|
+
"prettier": "^3.8.1",
|
|
40
|
+
"prettier-plugin-svelte": "^3.4.1",
|
|
41
|
+
"typescript-eslint": "^8.54.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
%sveltekit.head%
|
|
7
|
+
</head>
|
|
8
|
+
<body data-sveltekit-preload-data="hover">
|
|
9
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Handle } from "@sveltejs/kit";
|
|
2
|
+
import { auth } from "$lib/server/auth";
|
|
3
|
+
import { svelteKitHandler } from "better-auth/svelte-kit";
|
|
4
|
+
import { building } from "$app/environment";
|
|
5
|
+
|
|
6
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
7
|
+
const session = await auth.api.getSession({
|
|
8
|
+
headers: event.request.headers,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
event.locals.user = session?.user ?? null;
|
|
12
|
+
event.locals.session = session?.session ?? null;
|
|
13
|
+
|
|
14
|
+
return svelteKitHandler({ event, resolve, auth, building });
|
|
15
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { magicLink } from "better-auth/plugins";
|
|
4
|
+
import { sveltekitCookies } from "better-auth/svelte-kit";
|
|
5
|
+
import { getRequestEvent } from "$app/server";
|
|
6
|
+
import { building } from "$app/environment";
|
|
7
|
+
import { env } from "$env/dynamic/private";
|
|
8
|
+
import { db } from "./db";
|
|
9
|
+
import * as schema from "./db/schema";
|
|
10
|
+
import { notif } from "./notifications";
|
|
11
|
+
|
|
12
|
+
let _auth: Auth | undefined;
|
|
13
|
+
|
|
14
|
+
function createAuth() {
|
|
15
|
+
return betterAuth({
|
|
16
|
+
baseURL: env.BETTER_AUTH_URL,
|
|
17
|
+
database: drizzleAdapter(db, {
|
|
18
|
+
provider: "pg",
|
|
19
|
+
schema,
|
|
20
|
+
}),
|
|
21
|
+
plugins: [
|
|
22
|
+
magicLink({
|
|
23
|
+
sendMagicLink: async ({ email, url }) => {
|
|
24
|
+
await notif.send({
|
|
25
|
+
type: "magic-link",
|
|
26
|
+
userId: email, // before user exists, use email as userId
|
|
27
|
+
recipientEmail: email,
|
|
28
|
+
data: { url },
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
sveltekitCookies(getRequestEvent),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Auth = ReturnType<typeof createAuth>;
|
|
38
|
+
|
|
39
|
+
export const auth: Auth = new Proxy({} as Auth, {
|
|
40
|
+
get(_, prop) {
|
|
41
|
+
if (building) {
|
|
42
|
+
throw new Error("Cannot access auth during build");
|
|
43
|
+
}
|
|
44
|
+
if (!_auth) {
|
|
45
|
+
_auth = createAuth();
|
|
46
|
+
}
|
|
47
|
+
return (_auth as unknown as Record<string | symbol, unknown>)[prop];
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type Session = Auth["$Infer"]["Session"]["session"];
|
|
52
|
+
export type User = Auth["$Infer"]["Session"]["user"];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
import * as schema from "./schema";
|
|
4
|
+
import { env } from "$env/dynamic/private";
|
|
5
|
+
|
|
6
|
+
export type DrizzleDB = ReturnType<typeof drizzle<typeof schema>>;
|
|
7
|
+
|
|
8
|
+
let _db: DrizzleDB | undefined;
|
|
9
|
+
|
|
10
|
+
export const db = new Proxy({} as DrizzleDB, {
|
|
11
|
+
get(_, prop) {
|
|
12
|
+
if (!_db) {
|
|
13
|
+
if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set");
|
|
14
|
+
const client = postgres(env.DATABASE_URL);
|
|
15
|
+
_db = drizzle(client, { schema, casing: "snake_case" });
|
|
16
|
+
}
|
|
17
|
+
return _db[prop as keyof typeof _db];
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { boolean, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
// better-auth tables (managed by better-auth, do not insert/update directly)
|
|
4
|
+
export const user = pgTable("user", {
|
|
5
|
+
id: text("id").primaryKey(),
|
|
6
|
+
name: text("name").notNull(),
|
|
7
|
+
email: text("email").notNull().unique(),
|
|
8
|
+
emailVerified: timestamp("email_verified"),
|
|
9
|
+
image: text("image"),
|
|
10
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
11
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const session = pgTable("session", {
|
|
15
|
+
id: text("id").primaryKey(),
|
|
16
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
17
|
+
token: text("token").notNull().unique(),
|
|
18
|
+
ipAddress: text("ip_address"),
|
|
19
|
+
userAgent: text("user_agent"),
|
|
20
|
+
userId: text("user_id")
|
|
21
|
+
.notNull()
|
|
22
|
+
.references(() => user.id),
|
|
23
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
24
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const account = pgTable("account", {
|
|
28
|
+
id: text("id").primaryKey(),
|
|
29
|
+
accountId: text("account_id").notNull(),
|
|
30
|
+
providerId: text("provider_id").notNull(),
|
|
31
|
+
userId: text("user_id")
|
|
32
|
+
.notNull()
|
|
33
|
+
.references(() => user.id),
|
|
34
|
+
accessToken: text("access_token"),
|
|
35
|
+
refreshToken: text("refresh_token"),
|
|
36
|
+
idToken: text("id_token"),
|
|
37
|
+
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
|
38
|
+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
|
39
|
+
scope: text("scope"),
|
|
40
|
+
password: text("password"),
|
|
41
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
42
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const verification = pgTable("verification", {
|
|
46
|
+
id: text("id").primaryKey(),
|
|
47
|
+
identifier: text("identifier").notNull(),
|
|
48
|
+
value: text("value").notNull(),
|
|
49
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
50
|
+
createdAt: timestamp("created_at"),
|
|
51
|
+
updatedAt: timestamp("updated_at"),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Notification tables (managed by @mikstack/notifications)
|
|
55
|
+
export const notificationDelivery = pgTable("notification_delivery", {
|
|
56
|
+
id: text("id")
|
|
57
|
+
.primaryKey()
|
|
58
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
59
|
+
userId: text("user_id")
|
|
60
|
+
.notNull()
|
|
61
|
+
.references(() => user.id),
|
|
62
|
+
type: text("type").notNull(),
|
|
63
|
+
channel: text("channel").notNull(),
|
|
64
|
+
status: text("status", { enum: ["pending", "sent", "delivered", "failed"] })
|
|
65
|
+
.notNull()
|
|
66
|
+
.default("pending"),
|
|
67
|
+
content: jsonb("content"),
|
|
68
|
+
error: text("error"),
|
|
69
|
+
retryOf: text("retry_of"),
|
|
70
|
+
retriesLeft: integer("retries_left").notNull().default(0),
|
|
71
|
+
recipientEmail: text("recipient_email"),
|
|
72
|
+
externalId: text("external_id"),
|
|
73
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
74
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const inAppNotification = pgTable("in_app_notification", {
|
|
78
|
+
id: text("id")
|
|
79
|
+
.primaryKey()
|
|
80
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
81
|
+
userId: text("user_id")
|
|
82
|
+
.notNull()
|
|
83
|
+
.references(() => user.id),
|
|
84
|
+
type: text("type").notNull(),
|
|
85
|
+
title: text("title").notNull(),
|
|
86
|
+
body: text("body"),
|
|
87
|
+
url: text("url"),
|
|
88
|
+
icon: text("icon"),
|
|
89
|
+
read: boolean("read").notNull().default(false),
|
|
90
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const notificationPreference = pgTable("notification_preference", {
|
|
94
|
+
id: text("id")
|
|
95
|
+
.primaryKey()
|
|
96
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
97
|
+
userId: text("user_id")
|
|
98
|
+
.notNull()
|
|
99
|
+
.references(() => user.id),
|
|
100
|
+
notificationType: text("notification_type").notNull(),
|
|
101
|
+
channel: text("channel").notNull(),
|
|
102
|
+
enabled: boolean("enabled").notNull(),
|
|
103
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Application tables
|
|
107
|
+
|
|
108
|
+
export const note = pgTable("note", {
|
|
109
|
+
id: text("id").primaryKey(),
|
|
110
|
+
title: text("title").notNull(),
|
|
111
|
+
content: text("content").notNull().default(""),
|
|
112
|
+
userId: text("user_id")
|
|
113
|
+
.notNull()
|
|
114
|
+
.references(() => user.id),
|
|
115
|
+
createdAt: timestamp("created_at").notNull(),
|
|
116
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
117
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database seed script.
|
|
3
|
+
*
|
|
4
|
+
* Run with: {{pmRun}} db:seed
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// import { db } from './index';
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
10
|
+
async function seed() {
|
|
11
|
+
console.log("Seeding database...");
|
|
12
|
+
|
|
13
|
+
// Add your seed data here
|
|
14
|
+
|
|
15
|
+
console.log("Seeding complete.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
seed().catch((err) => {
|
|
19
|
+
console.error("Seed failed:", err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|