vyft 0.1.0-alpha

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/build.d.ts +11 -0
  4. package/dist/build.js +39 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +200 -0
  7. package/dist/docker.d.ts +48 -0
  8. package/dist/docker.js +855 -0
  9. package/dist/exec.d.ts +2 -0
  10. package/dist/exec.js +28 -0
  11. package/dist/index.d.ts +6 -0
  12. package/dist/index.js +3 -0
  13. package/dist/init.d.ts +1 -0
  14. package/dist/init.js +100 -0
  15. package/dist/interpolate.d.ts +2 -0
  16. package/dist/interpolate.js +8 -0
  17. package/dist/logger.d.ts +2 -0
  18. package/dist/logger.js +10 -0
  19. package/dist/resource.d.ts +78 -0
  20. package/dist/resource.js +35 -0
  21. package/dist/runtime.d.ts +6 -0
  22. package/dist/runtime.js +0 -0
  23. package/dist/swarm/factories.d.ts +8 -0
  24. package/dist/swarm/factories.js +50 -0
  25. package/dist/swarm/index.d.ts +10 -0
  26. package/dist/swarm/index.js +5 -0
  27. package/dist/swarm/types.d.ts +25 -0
  28. package/dist/swarm/types.js +0 -0
  29. package/dist/symbols.d.ts +8 -0
  30. package/dist/symbols.js +1 -0
  31. package/package.json +68 -0
  32. package/templates/fullstack/apps/api/Dockerfile +21 -0
  33. package/templates/fullstack/apps/api/package.json +26 -0
  34. package/templates/fullstack/apps/api/src/auth.ts +21 -0
  35. package/templates/fullstack/apps/api/src/db.ts +16 -0
  36. package/templates/fullstack/apps/api/src/index.ts +17 -0
  37. package/templates/fullstack/apps/api/src/router.ts +11 -0
  38. package/templates/fullstack/apps/api/src/schema.ts +11 -0
  39. package/templates/fullstack/apps/api/tsconfig.json +8 -0
  40. package/templates/fullstack/apps/web/index.html +12 -0
  41. package/templates/fullstack/apps/web/package.json +21 -0
  42. package/templates/fullstack/apps/web/src/app.tsx +8 -0
  43. package/templates/fullstack/apps/web/src/main.tsx +9 -0
  44. package/templates/fullstack/apps/web/tsconfig.json +7 -0
  45. package/templates/fullstack/apps/web/vite.config.ts +14 -0
  46. package/templates/fullstack/compose.yaml +14 -0
  47. package/templates/fullstack/dockerignore +7 -0
  48. package/templates/fullstack/gitignore +3 -0
  49. package/templates/fullstack/package.json +17 -0
  50. package/templates/fullstack/pnpm-workspace.yaml +2 -0
  51. package/templates/fullstack/tsconfig.json +11 -0
  52. package/templates/fullstack/vyft.config.ts +37 -0
package/dist/exec.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Logger } from "pino";
2
+ export declare function exec(command: string, cwd: string, env?: Record<string, string>, log?: Logger): Promise<void>;
package/dist/exec.js ADDED
@@ -0,0 +1,28 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function exec(command, cwd, env, log) {
3
+ const start = performance.now();
4
+ log?.debug({ command, cwd }, "process spawning");
5
+ return new Promise((resolve, reject) => {
6
+ const proc = spawn(command, {
7
+ cwd,
8
+ stdio: "inherit",
9
+ shell: true,
10
+ env: env ? { ...process.env, ...env } : undefined,
11
+ });
12
+ proc.on("close", (code) => {
13
+ const durationMs = Math.round(performance.now() - start);
14
+ if (code !== 0) {
15
+ log?.debug({ command, cwd, exitCode: code, durationMs }, "process exited with error");
16
+ reject(new Error(`Command failed with exit code ${code}`));
17
+ }
18
+ else {
19
+ log?.debug({ command, cwd, exitCode: 0, durationMs }, "process exited");
20
+ resolve();
21
+ }
22
+ });
23
+ proc.on("error", (err) => {
24
+ log?.debug({ err, command, cwd }, "process spawn failed");
25
+ reject(err);
26
+ });
27
+ });
28
+ }
@@ -0,0 +1,6 @@
1
+ export { interpolate } from "./interpolate.js";
2
+ export type { EnvValue, HealthCheckConfig, Interpolation, Reference, Resource, ResourceLimits, ResourceType, Secret, SecretConfig, Service, ServiceConfig, Site, SiteConfig, Volume, VolumeConfig, } from "./resource.js";
3
+ export { isInterpolation, isReference, isSecret, validateId, validateRoute, } from "./resource.js";
4
+ export type { Runtime } from "./runtime.js";
5
+ export type { RuntimeMeta, RuntimeRef } from "./symbols.js";
6
+ export { VYFT_RUNTIME } from "./symbols.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { interpolate } from "./interpolate.js";
2
+ export { isInterpolation, isReference, isSecret, validateId, validateRoute, } from "./resource.js";
3
+ export { VYFT_RUNTIME } from "./symbols.js";
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function init(directory?: string): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,100 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { confirm, intro, isCancel, log, outro, text } from "@clack/prompts";
5
+ const TEMPLATES_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../templates/fullstack");
6
+ async function walkDir(dir) {
7
+ const files = [];
8
+ const entries = await readdir(dir, { withFileTypes: true });
9
+ for (const entry of entries) {
10
+ const full = path.join(dir, entry.name);
11
+ if (entry.isDirectory()) {
12
+ files.push(...(await walkDir(full)));
13
+ }
14
+ else {
15
+ files.push(full);
16
+ }
17
+ }
18
+ return files;
19
+ }
20
+ async function dirExists(dir) {
21
+ try {
22
+ const s = await stat(dir);
23
+ return s.isDirectory();
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function dirIsNonEmpty(dir) {
30
+ try {
31
+ const entries = await readdir(dir);
32
+ return entries.length > 0;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ export async function init(directory) {
39
+ intro("Create a new project");
40
+ const name = await text({
41
+ message: "Project name",
42
+ placeholder: "my-app",
43
+ defaultValue: directory || "my-app",
44
+ validate(value) {
45
+ if (!value)
46
+ return "Project name is required";
47
+ if (!/^[a-z0-9-]+$/.test(value))
48
+ return "Use lowercase letters, numbers, and hyphens only";
49
+ },
50
+ });
51
+ if (isCancel(name))
52
+ return;
53
+ const domain = await text({
54
+ message: "Domain",
55
+ placeholder: "example.com",
56
+ validate(value) {
57
+ if (!value)
58
+ return "Domain is required";
59
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(value))
60
+ return "Enter a valid domain (e.g. example.com)";
61
+ },
62
+ });
63
+ if (isCancel(domain))
64
+ return;
65
+ const targetDir = path.resolve(process.cwd(), directory || name);
66
+ if ((await dirExists(targetDir)) && (await dirIsNonEmpty(targetDir))) {
67
+ const ok = await confirm({
68
+ message: `Directory ${path.basename(targetDir)} is not empty. Continue?`,
69
+ });
70
+ if (isCancel(ok) || !ok)
71
+ return;
72
+ }
73
+ const templateFiles = await walkDir(TEMPLATES_DIR);
74
+ const replacements = {
75
+ "{{name}}": name,
76
+ "{{domain}}": domain,
77
+ };
78
+ for (const templateFile of templateFiles) {
79
+ let relativePath = path.relative(TEMPLATES_DIR, templateFile);
80
+ if (relativePath === "gitignore")
81
+ relativePath = ".gitignore";
82
+ if (relativePath === "dockerignore")
83
+ relativePath = ".dockerignore";
84
+ const destPath = path.join(targetDir, relativePath);
85
+ await mkdir(path.dirname(destPath), { recursive: true });
86
+ let content = await readFile(templateFile, "utf-8");
87
+ for (const [placeholder, value] of Object.entries(replacements)) {
88
+ content = content.replaceAll(placeholder, value);
89
+ }
90
+ await writeFile(destPath, content);
91
+ log.step(relativePath);
92
+ }
93
+ outro("Project created");
94
+ console.log();
95
+ console.log("Next steps:");
96
+ console.log(` cd ${path.relative(process.cwd(), targetDir)}`);
97
+ console.log(" pnpm install");
98
+ console.log(" pnpm dev");
99
+ console.log();
100
+ }
@@ -0,0 +1,2 @@
1
+ import type { Interpolation, Reference } from "./resource.js";
2
+ export declare function interpolate(strings: TemplateStringsArray, ...values: Array<Reference | string>): Interpolation;
@@ -0,0 +1,8 @@
1
+ import { isReference } from "./resource.js";
2
+ export function interpolate(strings, ...values) {
3
+ const hasRef = values.some(isReference);
4
+ if (!hasRef) {
5
+ throw new Error("interpolate() requires at least one Reference — use a plain string instead");
6
+ }
7
+ return { type: "interpolation", strings, values };
8
+ }
@@ -0,0 +1,2 @@
1
+ import pino from "pino";
2
+ export declare const logger: pino.Logger<never, boolean>;
package/dist/logger.js ADDED
@@ -0,0 +1,10 @@
1
+ import pino from "pino";
2
+ function resolveLevel() {
3
+ const val = process.env.LOG_LEVEL;
4
+ if (!val)
5
+ return "silent";
6
+ return val;
7
+ }
8
+ export const logger = pino({
9
+ level: resolveLevel(),
10
+ });
@@ -0,0 +1,78 @@
1
+ import type { RuntimeRef } from "./symbols.js";
2
+ export type ResourceType = "volume" | "secret" | "service" | "site";
3
+ export type VolumeConfig = {};
4
+ export interface SecretConfig {
5
+ length?: number;
6
+ }
7
+ export interface HealthCheckConfig {
8
+ command: string[];
9
+ interval?: string;
10
+ timeout?: string;
11
+ retries?: number;
12
+ startPeriod?: string;
13
+ }
14
+ export interface ResourceLimits {
15
+ memory?: string;
16
+ cpus?: number;
17
+ }
18
+ export interface Volume extends RuntimeRef {
19
+ type: "volume";
20
+ id: string;
21
+ config: VolumeConfig;
22
+ }
23
+ export interface Secret extends RuntimeRef {
24
+ type: "secret";
25
+ id: string;
26
+ config: SecretConfig;
27
+ }
28
+ export interface Service extends RuntimeRef {
29
+ type: "service";
30
+ id: string;
31
+ config: ServiceConfig;
32
+ host: string;
33
+ port: number;
34
+ url: string;
35
+ }
36
+ export interface Site extends RuntimeRef {
37
+ type: "site";
38
+ id: string;
39
+ config: SiteConfig;
40
+ url: string;
41
+ }
42
+ export type Resource = Volume | Secret | Service | Site;
43
+ export type Reference = Secret;
44
+ export interface Interpolation {
45
+ type: "interpolation";
46
+ strings: TemplateStringsArray;
47
+ values: Array<Reference | string>;
48
+ }
49
+ export type EnvValue = string | Reference | Interpolation;
50
+ export declare function isReference(value: unknown): value is Reference;
51
+ export declare function isSecret(value: unknown): value is Secret;
52
+ export declare function isInterpolation(value: unknown): value is Interpolation;
53
+ export interface ServiceConfig {
54
+ image: string | {
55
+ context?: string;
56
+ dockerfile?: string;
57
+ };
58
+ route?: string;
59
+ port?: number;
60
+ env?: Record<string, EnvValue>;
61
+ command?: string[];
62
+ volumes?: Array<{
63
+ volume: Volume;
64
+ mount: string;
65
+ }>;
66
+ }
67
+ export interface SiteConfig {
68
+ route: string;
69
+ spa?: boolean;
70
+ build: {
71
+ cwd: string;
72
+ output?: string;
73
+ command?: string;
74
+ env?: Record<string, string>;
75
+ };
76
+ }
77
+ export declare function validateId(id: string): void;
78
+ export declare function validateRoute(route: string): void;
@@ -0,0 +1,35 @@
1
+ export function isReference(value) {
2
+ return isSecret(value);
3
+ }
4
+ export function isSecret(value) {
5
+ return (typeof value === "object" &&
6
+ value !== null &&
7
+ value.type === "secret");
8
+ }
9
+ export function isInterpolation(value) {
10
+ return (typeof value === "object" &&
11
+ value !== null &&
12
+ value.type === "interpolation");
13
+ }
14
+ // Validation
15
+ const ID_PATTERN = /^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/;
16
+ const ROUTE_PATTERN = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(\/.+)?$/;
17
+ export function validateId(id) {
18
+ if (!id || id.length < 1) {
19
+ throw new Error("Resource ID cannot be empty");
20
+ }
21
+ if (id.length > 63) {
22
+ throw new Error("Resource ID cannot exceed 63 characters");
23
+ }
24
+ if (!ID_PATTERN.test(id)) {
25
+ throw new Error(`Invalid resource ID "${id}": must start with a letter, contain only lowercase letters, numbers, and hyphens, and end with a letter or number`);
26
+ }
27
+ }
28
+ export function validateRoute(route) {
29
+ if (!route) {
30
+ throw new Error("Route cannot be empty");
31
+ }
32
+ if (!ROUTE_PATTERN.test(route)) {
33
+ throw new Error(`Invalid route "${route}": must be a valid domain with optional path`);
34
+ }
35
+ }
@@ -0,0 +1,6 @@
1
+ import type { Resource } from "./resource.js";
2
+ export interface Runtime {
3
+ create(resource: Resource): Promise<void>;
4
+ exists(resource: Resource): Promise<boolean>;
5
+ remove(resource: Resource): Promise<void>;
6
+ }
File without changes
@@ -0,0 +1,8 @@
1
+ import type { Secret, SecretConfig, Service, Site, SiteConfig, Volume } from "../resource.js";
2
+ import type { StorageDriver, SwarmConfig, SwarmServiceConfig, SwarmVolumeConfig } from "./types.js";
3
+ export declare function createFactories<D extends StorageDriver = "local">(swarmConfig: SwarmConfig<D>): {
4
+ volume(id: string, config?: SwarmVolumeConfig<D>): Volume;
5
+ service(id: string, config: SwarmServiceConfig): Service;
6
+ secret(id: string, config?: SecretConfig): Secret;
7
+ site(id: string, config: SiteConfig): Site;
8
+ };
@@ -0,0 +1,50 @@
1
+ import { validateId, validateRoute } from "../resource.js";
2
+ import { VYFT_RUNTIME } from "../symbols.js";
3
+ function attachRuntime(obj, config) {
4
+ return Object.assign(obj, {
5
+ [VYFT_RUNTIME]: { name: "swarm", config },
6
+ });
7
+ }
8
+ export function createFactories(swarmConfig) {
9
+ return {
10
+ volume(id, config = {}) {
11
+ validateId(id);
12
+ return attachRuntime({ type: "volume", id, config }, swarmConfig);
13
+ },
14
+ service(id, config) {
15
+ validateId(id);
16
+ if (config.route) {
17
+ validateRoute(config.route);
18
+ }
19
+ const port = config.port || 3000;
20
+ return attachRuntime({
21
+ type: "service",
22
+ id,
23
+ config,
24
+ host: id,
25
+ port,
26
+ url: config.route
27
+ ? `https://${config.route}`
28
+ : `http://${id}:${port}`,
29
+ }, swarmConfig);
30
+ },
31
+ secret(id, config = {}) {
32
+ validateId(id);
33
+ return attachRuntime({
34
+ type: "secret",
35
+ id,
36
+ config,
37
+ }, swarmConfig);
38
+ },
39
+ site(id, config) {
40
+ validateId(id);
41
+ validateRoute(config.route);
42
+ return attachRuntime({
43
+ type: "site",
44
+ id,
45
+ config,
46
+ url: `https://${config.route}`,
47
+ }, swarmConfig);
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,10 @@
1
+ export { createFactories } from "./factories.js";
2
+ export type { DriverVolumeOptions, StorageDriver, SwarmConfig, SwarmServiceConfig, SwarmVolumeConfig, } from "./types.js";
3
+ import type { Secret, SecretConfig, Service, Site, SiteConfig, Volume } from "../resource.js";
4
+ import type { StorageDriver, SwarmConfig, SwarmServiceConfig, SwarmVolumeConfig } from "./types.js";
5
+ export declare function swarm<D extends StorageDriver = "local">(config?: SwarmConfig<D>): {
6
+ volume: (id: string, config?: SwarmVolumeConfig<D>) => Volume;
7
+ service: (id: string, config: SwarmServiceConfig) => Service;
8
+ secret: (id: string, config?: SecretConfig) => Secret;
9
+ site: (id: string, config: SiteConfig) => Site;
10
+ };
@@ -0,0 +1,5 @@
1
+ export { createFactories } from "./factories.js";
2
+ import { createFactories } from "./factories.js";
3
+ export function swarm(config) {
4
+ return createFactories(config ?? {});
5
+ }
@@ -0,0 +1,25 @@
1
+ import type { HealthCheckConfig, ResourceLimits, ServiceConfig, VolumeConfig } from "../resource.js";
2
+ export interface DriverVolumeOptions {
3
+ local: {
4
+ size?: string;
5
+ };
6
+ overlay2: {};
7
+ btrfs: {
8
+ size?: string;
9
+ };
10
+ zfs: {
11
+ size?: string;
12
+ compression?: string;
13
+ };
14
+ }
15
+ export type StorageDriver = keyof DriverVolumeOptions;
16
+ export interface SwarmConfig<D extends StorageDriver = "local"> {
17
+ storageDriver?: D;
18
+ }
19
+ export type SwarmVolumeConfig<D extends StorageDriver = "local"> = VolumeConfig & DriverVolumeOptions[D];
20
+ export interface SwarmServiceConfig extends ServiceConfig {
21
+ replicas?: number;
22
+ healthCheck?: HealthCheckConfig;
23
+ resources?: ResourceLimits;
24
+ restartPolicy?: "none" | "on-failure" | "any";
25
+ }
File without changes
@@ -0,0 +1,8 @@
1
+ export declare const VYFT_RUNTIME: unique symbol;
2
+ export interface RuntimeMeta {
3
+ name: string;
4
+ config: Record<string, unknown>;
5
+ }
6
+ export interface RuntimeRef {
7
+ readonly [VYFT_RUNTIME]: RuntimeMeta;
8
+ }
@@ -0,0 +1 @@
1
+ export const VYFT_RUNTIME = Symbol.for("vyft.runtime");
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "vyft",
3
+ "version": "0.1.0-alpha",
4
+ "description": "Deploy apps to Docker Swarm with TypeScript",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "vyft": "./dist/cli.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./swarm": {
18
+ "types": "./dist/swarm/index.d.ts",
19
+ "import": "./dist/swarm/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "templates"
25
+ ],
26
+ "keywords": [
27
+ "docker",
28
+ "swarm",
29
+ "deploy",
30
+ "infrastructure",
31
+ "devops",
32
+ "typescript"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/vyftlabs/vyft.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/vyftlabs/vyft/issues"
40
+ },
41
+ "homepage": "https://github.com/vyftlabs/vyft#readme",
42
+ "devDependencies": {
43
+ "@biomejs/biome": "^2.4.4",
44
+ "@types/dockerode": "^4.0.1",
45
+ "@types/node": "^25.3.1",
46
+ "@types/tar-fs": "^2.0.4",
47
+ "tsx": "^4.21.0",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.0.18"
50
+ },
51
+ "dependencies": {
52
+ "@clack/prompts": "^1.0.1",
53
+ "commander": "^14.0.3",
54
+ "dockerode": "^4.0.9",
55
+ "pino": "^10.3.1",
56
+ "tar-fs": "^3.1.1"
57
+ },
58
+ "engines": {
59
+ "node": ">=22"
60
+ },
61
+ "scripts": {
62
+ "build": "tsc",
63
+ "lint": "biome check src/",
64
+ "format:check": "biome format src/",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest"
67
+ }
68
+ }
@@ -0,0 +1,21 @@
1
+ FROM node:22-slim AS base
2
+ RUN corepack enable
3
+
4
+ FROM base AS deps
5
+ WORKDIR /app
6
+ COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
7
+ COPY apps/api/package.json ./apps/api/
8
+ RUN pnpm install --frozen-lockfile
9
+
10
+ FROM deps AS build
11
+ COPY apps/api/ ./apps/api/
12
+ RUN pnpm --filter {{name}}-api build
13
+
14
+ FROM build AS deploy
15
+ RUN pnpm --filter {{name}}-api --prod deploy --legacy /prod/api
16
+
17
+ FROM base
18
+ WORKDIR /app
19
+ COPY --from=deploy /prod/api .
20
+ EXPOSE 3000
21
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{name}}-api",
3
+ "private": true,
4
+ "type": "module",
5
+ "files": ["dist"],
6
+ "scripts": {
7
+ "dev": "tsx watch --env-file=../../.env src/index.ts",
8
+ "build": "tsc"
9
+ },
10
+ "dependencies": {
11
+ "@hono/node-server": "^1.14.1",
12
+ "@hono/trpc-server": "^0.3.4",
13
+ "@trpc/server": "^11.1.2",
14
+ "better-auth": "^1.2.8",
15
+ "drizzle-orm": "^0.44.2",
16
+ "hono": "^4.7.10",
17
+ "postgres": "^3.4.7",
18
+ "zod": "^3.25.17"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.15.0",
22
+ "drizzle-kit": "^0.31.1",
23
+ "tsx": "^4.19.4",
24
+ "typescript": "^5.8.3"
25
+ }
26
+ }
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { betterAuth } from 'better-auth';
3
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
4
+ import { db } from './db.js';
5
+
6
+ function env(key: string): string {
7
+ const fileKey = `${key}_FILE`;
8
+ const filePath = process.env[fileKey];
9
+ if (filePath) return readFileSync(filePath, 'utf-8').trim();
10
+ const value = process.env[key];
11
+ if (value) return value;
12
+ throw new Error(`Missing env var: ${key} or ${fileKey}`);
13
+ }
14
+
15
+ export const auth = betterAuth({
16
+ database: drizzleAdapter(db, { provider: 'pg' }),
17
+ secret: env('AUTH_SECRET'),
18
+ basePath: '/api/auth',
19
+ baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
20
+ emailAndPassword: { enabled: true },
21
+ });
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { drizzle } from 'drizzle-orm/postgres-js';
3
+ import postgres from 'postgres';
4
+ import * as schema from './schema.js';
5
+
6
+ function env(key: string): string {
7
+ const fileKey = `${key}_FILE`;
8
+ const filePath = process.env[fileKey];
9
+ if (filePath) return readFileSync(filePath, 'utf-8').trim();
10
+ const value = process.env[key];
11
+ if (value) return value;
12
+ throw new Error(`Missing env var: ${key} or ${fileKey}`);
13
+ }
14
+
15
+ const client = postgres(env('DATABASE_URL'));
16
+ export const db = drizzle(client, { schema });
@@ -0,0 +1,17 @@
1
+ import { serve } from '@hono/node-server';
2
+ import { trpcServer } from '@hono/trpc-server';
3
+ import { Hono } from 'hono';
4
+ import { auth } from './auth.js';
5
+ import { appRouter } from './router.js';
6
+
7
+ const app = new Hono().basePath('/api');
8
+
9
+ app.on(['GET', 'POST'], '/auth/**', (c) => auth.handler(c.req.raw));
10
+
11
+ app.use('/trpc/*', trpcServer({ router: appRouter }));
12
+
13
+ app.get('/health', (c) => c.json({ ok: true }));
14
+
15
+ serve({ fetch: app.fetch, port: 3000 }, (info) => {
16
+ console.log(`Server running on http://localhost:${info.port}`);
17
+ });
@@ -0,0 +1,11 @@
1
+ import { initTRPC } from '@trpc/server';
2
+
3
+ const t = initTRPC.create();
4
+
5
+ export const appRouter = t.router({
6
+ hello: t.procedure.query(() => {
7
+ return { message: 'Hello from tRPC!' };
8
+ }),
9
+ });
10
+
11
+ export type AppRouter = typeof appRouter;
@@ -0,0 +1,11 @@
1
+ import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+
3
+ export const users = pgTable('users', {
4
+ id: text('id').primaryKey(),
5
+ name: text('name').notNull(),
6
+ email: text('email').notNull().unique(),
7
+ emailVerified: timestamp('email_verified'),
8
+ image: text('image'),
9
+ createdAt: timestamp('created_at').defaultNow().notNull(),
10
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
11
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,12 @@
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.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>