feat: Docker self-hosting setup
- Fix Dockerfiles to copy pnpm-lock.yaml (frozen-lockfile compliance) - Add migrate target to API Dockerfile using builder stage - Add migrate service to docker-compose that runs before API starts - Add AUTH_DISABLED env var bypass to auth middleware for dev/Docker - Proxy /api/ from nginx to API container (no CORS needed) - Include initial Drizzle migration (0000_colossal_colossus.sql) - Add .env.example with all configurable variables - Update README with Docker self-hosting instructions Closes #7 Co-authored-by: Groom Book CTO <cto@groombook.app> Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #16.
This commit is contained in:
committed by
GitHub
parent
4f92b8bffb
commit
820a5240d1
@@ -0,0 +1,16 @@
|
||||
# Groom Book — Environment Variables
|
||||
# Copy this file to .env and adjust values for your deployment.
|
||||
|
||||
# ── Database ──────────────────────────────────────────────────────────────────
|
||||
DATABASE_URL=postgres://groombook:groombook@postgres:5432/groombook
|
||||
|
||||
# ── Authentication ────────────────────────────────────────────────────────────
|
||||
# Set AUTH_DISABLED=true to skip OIDC validation (useful for local dev/Docker).
|
||||
# In production, configure an Authentik instance and set these values.
|
||||
AUTH_DISABLED=false
|
||||
OIDC_ISSUER=https://authentik.example.com
|
||||
OIDC_AUDIENCE=groombook
|
||||
|
||||
# ── API ───────────────────────────────────────────────────────────────────────
|
||||
PORT=3000
|
||||
CORS_ORIGIN=http://localhost:8080
|
||||
@@ -96,6 +96,49 @@ pnpm build
|
||||
|
||||
## Self-Hosting
|
||||
|
||||
### Docker Compose (recommended for single-server deployments)
|
||||
|
||||
The fastest way to run Groom Book is with Docker Compose. This starts PostgreSQL, runs database migrations, and serves both the API and web frontend.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/groombook/groombook.git
|
||||
cd groombook
|
||||
|
||||
# Start everything (Postgres + migrate + API + web)
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
- **Web UI**: http://localhost:8080
|
||||
- **API**: http://localhost:3000
|
||||
|
||||
The default `docker-compose.yml` sets `AUTH_DISABLED=true` so you can explore the app without configuring an OIDC provider. **Disable this in any internet-facing deployment.**
|
||||
|
||||
#### Production configuration
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Key variables to update for production:
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `DATABASE_URL` | PostgreSQL connection string |
|
||||
| `AUTH_DISABLED` | Set to `false` in production |
|
||||
| `OIDC_ISSUER` | Authentik issuer URL |
|
||||
| `OIDC_AUDIENCE` | OAuth2 audience (default: `groombook`) |
|
||||
| `CORS_ORIGIN` | Public URL of the web frontend |
|
||||
|
||||
To use your `.env` file with Docker Compose:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env up --build
|
||||
```
|
||||
|
||||
### Kubernetes (production-grade deployments)
|
||||
|
||||
See the [groombook/infra](https://github.com/groombook/infra) repository for Kubernetes manifests and Flux configuration.
|
||||
|
||||
Groom Book is deployed in the `groombook` Kubernetes namespace using:
|
||||
|
||||
+6
-2
@@ -4,7 +4,7 @@ WORKDIR /app
|
||||
|
||||
# Install deps
|
||||
FROM base AS deps
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY apps/api/package.json apps/api/
|
||||
COPY packages/db/package.json packages/db/
|
||||
COPY packages/types/package.json packages/types/
|
||||
@@ -22,7 +22,7 @@ RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY apps/api/package.json apps/api/
|
||||
COPY packages/db/package.json packages/db/
|
||||
COPY packages/types/package.json packages/types/
|
||||
@@ -34,3 +34,7 @@ COPY --from=builder /app/packages/types packages/types
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "apps/api/dist/index.js"]
|
||||
|
||||
# Migrate stage — runs drizzle-kit migrate against the database
|
||||
FROM builder AS migrate
|
||||
CMD ["pnpm", "db:migrate"]
|
||||
|
||||
@@ -24,6 +24,12 @@ export interface JwtPayload {
|
||||
}
|
||||
|
||||
export const authMiddleware: MiddlewareHandler = async (c, next) => {
|
||||
if (process.env.AUTH_DISABLED === "true") {
|
||||
c.set("jwtPayload", { sub: "dev-user" } as JwtPayload);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const authorization = c.req.header("Authorization");
|
||||
if (!authorization?.startsWith("Bearer ")) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ WORKDIR /app
|
||||
|
||||
# Install deps
|
||||
FROM base AS deps
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY apps/web/package.json apps/web/
|
||||
COPY packages/types/package.json packages/types/
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
@@ -9,6 +9,15 @@ server {
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Proxy API calls to the API service
|
||||
location /api/ {
|
||||
proxy_pass http://api:3000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# SPA fallback — serve index.html for all routes
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
+17
-3
@@ -15,6 +15,18 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
migrate:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/api/Dockerfile
|
||||
target: migrate
|
||||
environment:
|
||||
DATABASE_URL: postgres://groombook:groombook@postgres:5432/groombook
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: "no"
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
@@ -23,12 +35,14 @@ services:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://groombook:groombook@postgres:5432/groombook
|
||||
OIDC_ISSUER: http://authentik:9000
|
||||
OIDC_AUDIENCE: groombook
|
||||
CORS_ORIGIN: http://localhost:5173
|
||||
AUTH_DISABLED: "true"
|
||||
CORS_ORIGIN: http://localhost:8080
|
||||
PORT: "3000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
|
||||
web:
|
||||
build:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
CREATE TYPE "public"."appointment_status" AS ENUM('scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show');--> statement-breakpoint
|
||||
CREATE TYPE "public"."staff_role" AS ENUM('groomer', 'receptionist', 'manager');--> statement-breakpoint
|
||||
CREATE TABLE "appointments" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"client_id" uuid NOT NULL,
|
||||
"pet_id" uuid NOT NULL,
|
||||
"service_id" uuid NOT NULL,
|
||||
"staff_id" uuid,
|
||||
"status" "appointment_status" DEFAULT 'scheduled' NOT NULL,
|
||||
"start_time" timestamp NOT NULL,
|
||||
"end_time" timestamp NOT NULL,
|
||||
"notes" text,
|
||||
"price_cents" integer,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clients" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text,
|
||||
"phone" text,
|
||||
"address" text,
|
||||
"notes" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pets" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"client_id" uuid NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"species" text NOT NULL,
|
||||
"breed" text,
|
||||
"weight_kg" numeric(5, 2),
|
||||
"date_of_birth" timestamp,
|
||||
"grooming_notes" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "services" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"base_price_cents" integer NOT NULL,
|
||||
"duration_minutes" integer NOT NULL,
|
||||
"active" boolean DEFAULT true NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "staff" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"oidc_sub" text,
|
||||
"role" "staff_role" DEFAULT 'groomer' NOT NULL,
|
||||
"active" boolean DEFAULT true NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "staff_email_unique" UNIQUE("email"),
|
||||
CONSTRAINT "staff_oidc_sub_unique" UNIQUE("oidc_sub")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "appointments" ADD CONSTRAINT "appointments_client_id_clients_id_fk" FOREIGN KEY ("client_id") REFERENCES "public"."clients"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "appointments" ADD CONSTRAINT "appointments_pet_id_pets_id_fk" FOREIGN KEY ("pet_id") REFERENCES "public"."pets"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "appointments" ADD CONSTRAINT "appointments_service_id_services_id_fk" FOREIGN KEY ("service_id") REFERENCES "public"."services"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "appointments" ADD CONSTRAINT "appointments_staff_id_staff_id_fk" FOREIGN KEY ("staff_id") REFERENCES "public"."staff"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "pets" ADD CONSTRAINT "pets_client_id_clients_id_fk" FOREIGN KEY ("client_id") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"id": "477cddf9-970f-41c5-9cad-c1ed48c2bedf",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.appointments": {
|
||||
"name": "appointments",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"client_id": {
|
||||
"name": "client_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"pet_id": {
|
||||
"name": "pet_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"service_id": {
|
||||
"name": "service_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"staff_id": {
|
||||
"name": "staff_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "appointment_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'scheduled'"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "start_time",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"end_time": {
|
||||
"name": "end_time",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"price_cents": {
|
||||
"name": "price_cents",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"appointments_client_id_clients_id_fk": {
|
||||
"name": "appointments_client_id_clients_id_fk",
|
||||
"tableFrom": "appointments",
|
||||
"tableTo": "clients",
|
||||
"columnsFrom": [
|
||||
"client_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "restrict",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"appointments_pet_id_pets_id_fk": {
|
||||
"name": "appointments_pet_id_pets_id_fk",
|
||||
"tableFrom": "appointments",
|
||||
"tableTo": "pets",
|
||||
"columnsFrom": [
|
||||
"pet_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "restrict",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"appointments_service_id_services_id_fk": {
|
||||
"name": "appointments_service_id_services_id_fk",
|
||||
"tableFrom": "appointments",
|
||||
"tableTo": "services",
|
||||
"columnsFrom": [
|
||||
"service_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "restrict",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"appointments_staff_id_staff_id_fk": {
|
||||
"name": "appointments_staff_id_staff_id_fk",
|
||||
"tableFrom": "appointments",
|
||||
"tableTo": "staff",
|
||||
"columnsFrom": [
|
||||
"staff_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.clients": {
|
||||
"name": "clients",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"phone": {
|
||||
"name": "phone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"address": {
|
||||
"name": "address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.pets": {
|
||||
"name": "pets",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"client_id": {
|
||||
"name": "client_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"species": {
|
||||
"name": "species",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"breed": {
|
||||
"name": "breed",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"weight_kg": {
|
||||
"name": "weight_kg",
|
||||
"type": "numeric(5, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"date_of_birth": {
|
||||
"name": "date_of_birth",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"grooming_notes": {
|
||||
"name": "grooming_notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"pets_client_id_clients_id_fk": {
|
||||
"name": "pets_client_id_clients_id_fk",
|
||||
"tableFrom": "pets",
|
||||
"tableTo": "clients",
|
||||
"columnsFrom": [
|
||||
"client_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.services": {
|
||||
"name": "services",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"base_price_cents": {
|
||||
"name": "base_price_cents",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration_minutes": {
|
||||
"name": "duration_minutes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.staff": {
|
||||
"name": "staff",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"oidc_sub": {
|
||||
"name": "oidc_sub",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "staff_role",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'groomer'"
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"staff_email_unique": {
|
||||
"name": "staff_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"staff_oidc_sub_unique": {
|
||||
"name": "staff_oidc_sub_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"oidc_sub"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.appointment_status": {
|
||||
"name": "appointment_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"scheduled",
|
||||
"confirmed",
|
||||
"in_progress",
|
||||
"completed",
|
||||
"cancelled",
|
||||
"no_show"
|
||||
]
|
||||
},
|
||||
"public.staff_role": {
|
||||
"name": "staff_role",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"groomer",
|
||||
"receptionist",
|
||||
"manager"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1773771452946,
|
||||
"tag": "0000_colossal_colossus",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user