Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Backup-restore-övning (hardening-checklistan: "genomfört och protokollfört, kvartalsvis").
# Kör: DATABASE_URL=postgres://… ./infrastructure/deployment/backup-restore-test.sh
#
# Gör: logisk dump → återställ till temporär databas → jämför radantal per tabell
# → skriv protokoll → städa. Avslutar med exitkod 1 vid avvikelse.
set -euo pipefail
DATABASE_URL="${DATABASE_URL:?Sätt DATABASE_URL}"
# Roll med CREATEDB för test-databasen (RDS master/operatör). Faller tillbaka på DATABASE_URL.
ADMIN_URL="${ADMIN_DATABASE_URL:-$DATABASE_URL}"
STAMP=$(date +%Y%m%d_%H%M%S)
WORKDIR="${BACKUP_DIR:-/tmp/backup-restore-test}"
DUMP="$WORKDIR/dump-$STAMP.pgdump"
RESTORE_DB="restore_test_$STAMP"
PROTOKOLL="$WORKDIR/protokoll-$STAMP.md"
mkdir -p "$WORKDIR"
# Härled anslutning utan databasnamn för att kunna skapa/droppa test-databasen.
BASE_URL="${DATABASE_URL%/*}"
ADMIN_BASE_URL="${ADMIN_URL%/*}"
echo "==> 1/5 Dumpar databasen"
pg_dump "$DATABASE_URL" --format=custom --file="$DUMP"
SIZE=$(du -h "$DUMP" | cut -f1)
echo "==> 2/5 Skapar temporär databas $RESTORE_DB"
psql "$ADMIN_BASE_URL/postgres" -qc "CREATE DATABASE $RESTORE_DB" 2>/dev/null \
|| psql "$ADMIN_URL" -qc "CREATE DATABASE $RESTORE_DB"
cleanup() {
psql "$ADMIN_BASE_URL/postgres" -qc "DROP DATABASE IF EXISTS $RESTORE_DB" 2>/dev/null \
|| psql "$ADMIN_URL" -qc "DROP DATABASE IF EXISTS $RESTORE_DB" || true
}
trap cleanup EXIT
echo "==> 3/5 Återställer till $RESTORE_DB"
# Återställ som admin-rollen (äger test-databasen, får skapa extensions).
pg_restore --dbname="$ADMIN_BASE_URL/$RESTORE_DB" --no-owner --no-privileges "$DUMP"
echo "==> 4/5 Jämför radantal per tabell"
COUNT_SQL="SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY relname"
ROWS_SQL="SELECT string_agg(format('%s:%s', t.tablename, (xpath('/row/cnt/text()', query_to_xml(format('SELECT count(*) AS cnt FROM %I', t.tablename), false, true, '')))[1]::text), E'\n' ORDER BY t.tablename) FROM pg_tables t WHERE schemaname='public'"
ORIG=$(psql "$DATABASE_URL" -tA -c "$ROWS_SQL")
REST=$(psql "$ADMIN_BASE_URL/$RESTORE_DB" -tA -c "$ROWS_SQL")
STATUS="GODKÄND"
DIFF_OUT=""
if [ "$ORIG" != "$REST" ]; then
STATUS="UNDERKÄND"
DIFF_OUT=$(diff <(echo "$ORIG") <(echo "$REST") || true)
fi
echo "==> 5/5 Skriver protokoll: $PROTOKOLL"
TABLES=$(echo "$ORIG" | wc -l)
TOTAL=$(echo "$ORIG" | awk -F: '{s+=$2} END {print s}')
{
echo "# Backup-restore-protokoll $STAMP"
echo
echo "| Fält | Värde |"
echo "| ---- | ----- |"
echo "| Resultat | **$STATUS** |"
echo "| Dumpstorlek | $SIZE |"
echo "| Tabeller jämförda | $TABLES |"
echo "| Totalt antal rader | $TOTAL |"
echo "| Dump | $DUMP |"
echo
if [ -n "$DIFF_OUT" ]; then
echo '## Avvikelser'
echo '```'
echo "$DIFF_OUT"
echo '```'
else
echo "Alla tabellers radantal identiska mellan original och återställning."
fi
} > "$PROTOKOLL"
cat "$PROTOKOLL"
[ "$STATUS" = "GODKÄND" ]
@@ -0,0 +1,29 @@
-- Körs som RDS master-användare EN gång (spec §52).
-- Separat databas + användare med minsta möjliga privilegier.
-- Ingen delning av andras tabeller.
CREATE DATABASE app; -- byt namn per varumärke (brand.config.json slug)
-- Byt ut lösenordet mot ett genererat värde från Secrets Manager!
CREATE USER app_user WITH PASSWORD 'CHANGE_ME_FROM_SECRETS_MANAGER';
-- Anslut till den nya databasen och kör:
\connect app
-- Sök-extensions (i18n M7): skapas av master appanvändaren saknar rättighet.
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Låst schema: appanvändaren äger public-schemat i sin egen databas
GRANT CONNECT ON DATABASE app TO app_user;
GRANT USAGE, CREATE ON SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_user;
-- Ingen superuser, ingen replication, ingen åtkomst till andra databaser:
REVOKE ALL ON DATABASE postgres FROM app_user;
-- Migrationer körs av samma användare (CREATE på schemat räcker för drizzle).
-- Vid senare utbrytning till egen RDS: pg_dump/pg_restore enligt docs/infrastruktur.md.
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Deploy till befintlig server (spec §51, steg 1).
# Körs som den dedikerade app-användaren på servern.
set -euo pipefail
APP_DIR="/opt/${APP_SLUG:-app}-platform"
REPO_DIR="$APP_DIR/repo"
COMPOSE="$REPO_DIR/infrastructure/docker/docker-compose.production.yml"
echo "==> Deploy $(date -Iseconds)"
cd "$REPO_DIR"
git fetch --tags origin main
PREV_SHA=$(git rev-parse HEAD)
git reset --hard origin/main
NEW_SHA=$(git rev-parse HEAD)
echo "==> $PREV_SHA -> $NEW_SHA"
echo "==> Bygger images"
docker compose -f "$COMPOSE" build
echo "==> Kör databas-migrationer (med backup-punkt först)"
# RDS: automatiska snapshots finns; ta även logisk dump för snabb rollback.
mkdir -p "$APP_DIR/backups"
pg_dump "$DATABASE_URL" --format=custom \
--file="$APP_DIR/backups/pre-deploy-$(date +%Y%m%d-%H%M%S).dump" || {
echo "VARNING: pg_dump misslyckades avbryter deploy (spec §63: ingen migration utan backup)"; exit 1;
}
docker run --rm --env-file "$APP_DIR/secrets/api.env" app-api:latest \
node dist/index.js --migrate-only 2>/dev/null || true
# Migrationer körs normalt från CI/lokalt: pnpm db:migrate mot produktion via bastion.
echo "==> Startar om tjänster (rullande)"
docker compose -f "$COMPOSE" up -d app-redis
docker compose -f "$COMPOSE" up -d app-worker
docker compose -f "$COMPOSE" up -d app-api
docker compose -f "$COMPOSE" up -d app-admin
echo "==> Healthcheck"
sleep 5
if ! curl -fsS http://127.0.0.1:4100/healthz > /dev/null; then
echo "FEL: API svarar inte rollback till $PREV_SHA"
git reset --hard "$PREV_SHA"
docker compose -f "$COMPOSE" build app-api
docker compose -f "$COMPOSE" up -d app-api
exit 1
fi
echo "==> Deploy klar: $NEW_SHA"
+284
View File
@@ -0,0 +1,284 @@
#!/usr/bin/env bash
# ============================================================================
# FÖRSTA DEPLOY idempotent uppsättning av hela plattformen på en server.
#
# DEPLOY_MODE=staging ./infrastructure/deployment/first-deploy.sh
# DEPLOY_MODE=production ./infrastructure/deployment/first-deploy.sh
#
# STAGING (default): startar Postgres+Redis i Docker, genererar hemligheter
# om de saknas, AAMOS_MODE=mock tillåtet, seedar databasen, kör hela
# verifieringskedjan. Säker att köra om varje steg är idempotent.
#
# PRODUCTION: kräver befintlig .env med riktiga värden (skriptet VÄGRAR
# generera produktionshemligheter), kräver AAMOS_MODE=http, tar backup
# före migrationer, seedar ALDRIG automatiskt.
#
# Avslutar med exitkod 0 endast när ALLA grindar är gröna.
# ============================================================================
set -euo pipefail
MODE="${DEPLOY_MODE:-staging}"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
LOG_PREFIX="[first-deploy:$MODE]"
say() { echo "$LOG_PREFIX $*"; }
fail() { echo "$LOG_PREFIX FEL: $*" >&2; exit 1; }
say "Startar i läge: $MODE (repo: $ROOT)"
# ---------------------------------------------------------------------------
# GRIND 0: Förutsättningar
# ---------------------------------------------------------------------------
# Docker är VALFRITT: det används bara som bekvämlighet för att starta
# Postgres/Redis i staging när de inte redan svarar. Kör tjänsterna nativt
# (WSL, systemd, molndatabas) behövs ingen Docker alls Grind 2 avgör på
# faktisk nåbarhet, inte på verktygslistan.
if command -v docker >/dev/null && docker compose version >/dev/null 2>&1; then
DOCKER_OK=1
DOCKER_NOTE="docker+compose"
else
DOCKER_OK=0
DOCKER_NOTE="utan docker (ok om Postgres/Redis redan kör)"
say "OBS: docker saknas ingen fara om Postgres/Redis redan svarar (nativt/WSL/moln). Grind 2 verifierar."
fi
command -v node >/dev/null || fail "node saknas kräver Node >= 22."
NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])")
[ "$NODE_MAJOR" -ge 22 ] || fail "Node $NODE_MAJOR är för gammal kräver >= 22."
command -v pnpm >/dev/null || fail "pnpm saknas kör: corepack enable && corepack prepare pnpm@10 --activate"
say "GRIND 0 OK: $DOCKER_NOTE, node $NODE_MAJOR, pnpm $(pnpm --version)"
# ---------------------------------------------------------------------------
# GRIND 1: Miljöfil
# ---------------------------------------------------------------------------
if [ ! -f .env ]; then
if [ "$MODE" = "production" ]; then
fail ".env saknas. I produktion skapas den manuellt från .env.example med riktiga värden (Secrets Manager). Skriptet genererar ALDRIG produktionshemligheter."
fi
say "Ingen .env skapar staging-miljö med genererade hemligheter."
cp .env.example .env
gen() { node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"; }
# Ersätt dev-platshållare med riktiga slumpvärden (även staging förtjänar riktiga hemligheter).
sed -i "s|^JWT_ACCESS_SECRET=.*|JWT_ACCESS_SECRET=$(gen)|" .env
sed -i "s|^JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$(gen)|" .env
sed -i "s|^ENTITLEMENT_SIGNING_SECRET=.*|ENTITLEMENT_SIGNING_SECRET=$(gen)|" .env
# AAMOS: riktiga värden via miljövariabler vid körning vinner över mock.
if [ -n "${AAMOS_API_URL:-}" ] && [ -n "${AAMOS_API_KEY:-}" ]; then
sed -i "s|^AAMOS_MODE=.*|AAMOS_MODE=http|" .env
sed -i "s|^AAMOS_API_URL=.*|AAMOS_API_URL=$AAMOS_API_URL|" .env
sed -i "s|^AAMOS_API_KEY=.*|AAMOS_API_KEY=$AAMOS_API_KEY|" .env
say "AAMOS: riktiga uppgifter angivna kör http-läge från start."
else
sed -i "s|^AAMOS_MODE=.*|AAMOS_MODE=mock|" .env
fi
# E-post: riktig SMTP från start om uppgifter angivits.
if [ -n "${SMTP_HOST:-}" ] && [ -n "${MAIL_FROM:-}" ]; then
sed -i "s|^EMAIL_MODE=.*|EMAIL_MODE=smtp|" .env
sed -i "s|^SMTP_HOST=.*|SMTP_HOST=$SMTP_HOST|" .env
sed -i "s|^SMTP_PORT=.*|SMTP_PORT=${SMTP_PORT:-587}|" .env
sed -i "s|^SMTP_USER=.*|SMTP_USER=${SMTP_USER:-}|" .env
sed -i "s|^SMTP_PASS=.*|SMTP_PASS=${SMTP_PASS:-}|" .env
sed -i "s|^MAIL_FROM=.*|MAIL_FROM=$MAIL_FROM|" .env
say "E-post: SMTP-uppgifter angivna riktiga mejl från start."
fi
# Fillagring: riktig S3 från start om bucket angivits.
if [ -n "${S3_BUCKET:-}" ]; then
sed -i "s|^S3_MODE=.*|S3_MODE=aws|" .env
sed -i "s|^S3_BUCKET=.*|S3_BUCKET=$S3_BUCKET|" .env
[ -n "${S3_REGION:-}" ] && sed -i "s|^S3_REGION=.*|S3_REGION=$S3_REGION|" .env
[ -n "${S3_ENDPOINT:-}" ] && sed -i "s|^S3_ENDPOINT=.*|S3_ENDPOINT=$S3_ENDPOINT|" .env
[ -n "${S3_ACCESS_KEY_ID:-}" ] && sed -i "s|^S3_ACCESS_KEY_ID=.*|S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID|" .env
[ -n "${S3_SECRET_ACCESS_KEY:-}" ] && sed -i "s|^S3_SECRET_ACCESS_KEY=.*|S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY|" .env
say "Fillagring: S3-uppgifter angivna riktig lagring från start."
fi
chmod 600 .env
fi
get_env() { { grep -E "^$1=" .env || true; } | head -1 | cut -d= -f2-; }
AAMOS_MODE_VAL=$(get_env AAMOS_MODE)
if [ "$MODE" = "production" ]; then
[ "$AAMOS_MODE_VAL" = "http" ] || fail "Produktion kräver AAMOS_MODE=http (nu: '$AAMOS_MODE_VAL')."
[ -n "$(get_env AAMOS_API_URL)" ] || fail "AAMOS_API_URL saknas i .env."
[ -n "$(get_env AAMOS_API_KEY)" ] || fail "AAMOS_API_KEY saknas i .env."
for s in JWT_ACCESS_SECRET JWT_REFRESH_SECRET ENTITLEMENT_SIGNING_SECRET; do
case "$(get_env $s)" in dev-only-*|"") fail "$s har dev-/tomt värde sätt riktig hemlighet." ;; esac
done
fi
say "GRIND 1 OK: .env på plats ($([ "$MODE" = production ] && echo 'produktionsvärden verifierade' || echo 'staging'))"
# ---------------------------------------------------------------------------
# GRIND 2: Databas + Redis (nåbarhet avgör Docker är bara reservstart)
# ---------------------------------------------------------------------------
db_reachable() {
node -e "
const { Client } = require('pg');
const c = new Client({ connectionString: process.env.DB_URL, connectionTimeoutMillis: 3000 });
c.connect().then(() => c.end()).then(() => process.exit(0)).catch(() => process.exit(1));
" 2>/dev/null
}
# Rå TCP-PING: alla svar (även -NOAUTH) bevisar att Redis är nåbar.
redis_reachable() {
node -e "
const net = require('net');
let u; try { u = new URL(process.env.R_URL); } catch { process.exit(1); }
const s = net.createConnection({ host: u.hostname || '127.0.0.1', port: Number(u.port) || 6379 });
s.setTimeout(3000);
s.on('connect', () => s.write('PING\r\n'));
s.on('data', () => process.exit(0));
s.on('error', () => process.exit(1));
s.on('timeout', () => process.exit(1));
" 2>/dev/null
}
start_docker_services() {
# Reservstart i staging kräver Docker. Utan Docker: starta tjänsterna själv.
if [ "$DOCKER_OK" != 1 ] || ! docker info >/dev/null 2>&1; then
fail "$1 svarar inte och Docker är inte tillgänglig. Starta tjänsten nativt (t.ex. 'sudo service postgresql start' / 'sudo service redis-server start' på WSL) eller installera Docker och kör om skriptet."
fi
say "Startar Postgres + Redis i Docker (idempotent) …"
docker compose -f infrastructure/docker/docker-compose.dev.yml up -d
}
export DB_URL="$(get_env DATABASE_URL)"
export R_URL="$(get_env REDIS_URL)"
if DB_URL="$DB_URL" db_reachable; then
say "Databasen svarar redan ingen uppstart behövs."
elif [ "$MODE" = "staging" ]; then
start_docker_services "Databasen"
for i in $(seq 1 30); do
DB_URL="$DB_URL" db_reachable && break
sleep 1
[ "$i" = 30 ] && fail "Postgres svarade inte inom 30 s."
done
else
fail "DATABASE_URL svarar inte i produktion startar detta skript aldrig databaser. Kontrollera RDS/nätverk."
fi
if R_URL="$R_URL" redis_reachable; then
say "Redis svarar."
elif [ "$MODE" = "staging" ]; then
start_docker_services "Redis"
for i in $(seq 1 30); do
R_URL="$R_URL" redis_reachable && break
sleep 1
[ "$i" = 30 ] && fail "Redis svarade inte inom 30 s."
done
else
fail "REDIS_URL svarar inte i produktion startar detta skript aldrig tjänster. Kontrollera Redis/nätverk."
fi
say "GRIND 2 OK: databas + Redis nåbara"
# ---------------------------------------------------------------------------
# GRIND 3: Beroenden + verifiering av källkod
# ---------------------------------------------------------------------------
say "pnpm install (frozen lockfile) …"
pnpm install --frozen-lockfile >/dev/null
say "Varumärkesvakt …"
./scripts/brand-guard.sh
say "Typecheck + tester + build …"
pnpm typecheck >/dev/null
pnpm test >/dev/null
pnpm build >/dev/null
say "GRIND 3 OK: typecheck, tester, build, varumärkesvakt"
# ---------------------------------------------------------------------------
# GRIND 4: Migrationer (produktion: med backup före spec §63)
# ---------------------------------------------------------------------------
if [ "$MODE" = "production" ]; then
command -v pg_dump >/dev/null || fail "pg_dump saknas krävs för backup före migration."
mkdir -p "$ROOT/backups"
DBURL=$(get_env DATABASE_URL)
say "Backup före migration …"
pg_dump "$DBURL" --format=custom --file="$ROOT/backups/pre-first-deploy-$(date +%Y%m%d_%H%M%S).dump" \
|| fail "Backup misslyckades ingen migration utan backup (spec §63)."
fi
say "Kör migrationer …"
pnpm db:migrate
if [ "$MODE" = "staging" ]; then
say "Seedar databasen (endast staging) …"
pnpm db:seed
fi
say "GRIND 4 OK: databas migrerad$([ "$MODE" = staging ] && echo ' + seedad')"
# ---------------------------------------------------------------------------
# GRIND 5: AAMOS-utvärdering (spec §40)
# ---------------------------------------------------------------------------
say "Kör AAMOS-utvärderingssviten mot läge: $AAMOS_MODE_VAL"
pnpm eval:aamos || fail "Utvärderingssviten UNDERKÄND deploya inte (spec §40)."
say "GRIND 5 OK: eval-sviten GODKÄND"
# ---------------------------------------------------------------------------
# GRIND 6: Starta tjänsterna + hälsokontroll
# ---------------------------------------------------------------------------
API_PORT=$(get_env API_PORT); API_PORT=${API_PORT:-4000}
say "Startar API + worker (bakgrund) …"
# Idempotent omstart: döda ev. tidigare instanser från detta skript.
pkill -f "apps/api/dist/index.js" 2>/dev/null || true
pkill -f "apps/worker/dist/index.js" 2>/dev/null || true
sleep 1
mkdir -p "$ROOT/logs"
nohup node apps/api/dist/index.js >> "$ROOT/logs/api.log" 2>&1 &
nohup node apps/worker/dist/index.js >> "$ROOT/logs/worker.log" 2>&1 &
for i in $(seq 1 20); do
curl -fsS "http://127.0.0.1:$API_PORT/healthz" >/dev/null 2>&1 && break
sleep 1
[ "$i" = 20 ] && { tail -20 "$ROOT/logs/api.log"; fail "API svarade inte på /healthz inom 20 s."; }
done
curl -fsS "http://127.0.0.1:$API_PORT/healthz"
echo
curl -fsS "http://127.0.0.1:$API_PORT/readyz" >/dev/null || fail "/readyz underkänd DB/Redis-koppling trasig."
grep -q "Worker igång" "$ROOT/logs/worker.log" || sleep 3
grep -q "Worker igång" "$ROOT/logs/worker.log" || fail "Workern startade inte (se logs/worker.log)."
say "GRIND 6 OK: API + worker uppe, healthz/readyz gröna"
# ---------------------------------------------------------------------------
# GRIND 7: Funktionsrök (registrera -> logga in -> läsa recept)
# ---------------------------------------------------------------------------
SMOKE_EMAIL="smoke-$(date +%s)@example.com"
B="http://127.0.0.1:$API_PORT"
TOKEN=$(curl -fsS -X POST "$B/v1/auth/register" -H 'content-type: application/json' \
-d "{\"email\":\"$SMOKE_EMAIL\",\"password\":\"FirstDeploy123!\",\"displayName\":\"Deploy Smoke\",\"locale\":\"sv-SE\"}" \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).accessToken))")
[ -n "$TOKEN" ] || fail "Registrering i röktestet misslyckades."
RECIPES=$(curl -fsS "$B/v1/recipes?limit=1" -H "Authorization: Bearer $TOKEN" \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).recipes.length))")
if [ "$MODE" = "staging" ]; then
[ "$RECIPES" -ge 1 ] || fail "Röktestet hittade inga recept (seed saknas?)."
fi
say "GRIND 7 OK: register -> login -> recept fungerar"
# ---------------------------------------------------------------------------
# GRIND 8: Integrationsstatus vad är PÅ RIKTIGT och vad är mock?
# ---------------------------------------------------------------------------
EMAIL_MODE_VAL=$(get_env EMAIL_MODE); EMAIL_MODE_VAL=${EMAIL_MODE_VAL:-log}
S3_MODE_VAL=$(get_env S3_MODE); S3_MODE_VAL=${S3_MODE_VAL:-mock}
aamos_status=$([ "$AAMOS_MODE_VAL" = "http" ] && echo "RIKTIG (http)" || echo "mock")
email_status=$([ "$EMAIL_MODE_VAL" = "smtp" ] && echo "RIKTIG (smtp)" || echo "log (mejl till logs/api.log)")
s3_status=$([ "$S3_MODE_VAL" = "aws" ] && echo "RIKTIG (S3: $(get_env S3_BUCKET))" || echo "mock (lokal disk)")
say "-----------------------------------------------"
say "INTEGRATIONSSTATUS:"
say " AAMOS (AI): $aamos_status"
say " E-post: $email_status"
say " Fillagring: $s3_status"
say "-----------------------------------------------"
if [ "${REQUIRE_REAL:-0}" = "1" ]; then
MOCKS=""
[ "$AAMOS_MODE_VAL" != "http" ] && MOCKS="$MOCKS AAMOS(AAMOS_MODE=http+URL+nyckel)"
[ "$EMAIL_MODE_VAL" != "smtp" ] && MOCKS="$MOCKS E-post(EMAIL_MODE=smtp+SMTP_*+MAIL_FROM)"
[ "$S3_MODE_VAL" != "aws" ] && MOCKS="$MOCKS Fillagring(S3_MODE=aws+S3_*)"
if [ -n "$MOCKS" ]; then
fail "REQUIRE_REAL=1 men följande kör fortfarande mock:$MOCKS fyll i .env och kör om."
fi
say "GRIND 8 OK: REQUIRE_REAL uppfyllt alla integrationer är riktiga."
fi
# ---------------------------------------------------------------------------
# Klart
# ---------------------------------------------------------------------------
say "==============================================="
say "ALLA GRINDAR GRÖNA $MODE är uppe."
say "API: http://127.0.0.1:$API_PORT (Swagger: /docs)"
say "Loggar: $ROOT/logs/api.log, $ROOT/logs/worker.log"
if [ "$MODE" = "staging" ]; then
say "Admin: bygg finns i apps/admin/dist servera med valfri statisk server."
say "OBS: staging kör AAMOS_MODE=$AAMOS_MODE_VAL."
else
say "NÄSTA: peka reverse proxy (TLS) mot 127.0.0.1:$API_PORT och verifiera utifrån."
fi
say "==============================================="
+17
View File
@@ -0,0 +1,17 @@
# Admin byggs statiskt och serveras av nginx
FROM node:22-alpine AS builder
WORKDIR /build
RUN corepack enable
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./
COPY tsconfig.base.json turbo.json ./
COPY packages ./packages
COPY apps/admin ./apps/admin
ARG VITE_API_BASE_URL=http://localhost:4000
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN pnpm install --frozen-lockfile --filter @app/admin...
RUN pnpm --filter @app/admin build
FROM nginx:alpine AS runtime
COPY --from=builder /build/apps/admin/dist /usr/share/nginx/html
COPY infrastructure/docker/nginx-admin.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
+24
View File
@@ -0,0 +1,24 @@
# Food API multi-stage build (spec §5051)
FROM node:22-alpine AS builder
WORKDIR /build
RUN corepack enable
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./
COPY tsconfig.base.json turbo.json ./
COPY packages ./packages
COPY apps/api ./apps/api
RUN pnpm install --frozen-lockfile --filter @app/api...
RUN pnpm --filter @app/api build
FROM node:22-alpine AS runtime
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
ENV NODE_ENV=production
# tsup bundlar workspace-paketen; externa deps installeras separat
COPY --from=builder /build/apps/api/dist ./dist
COPY --from=builder /build/apps/api/package.json ./package.json
COPY --from=builder /build/node_modules ./node_modules
USER app
EXPOSE 4000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:4000/healthz || exit 1
CMD ["node", "dist/index.js"]
+20
View File
@@ -0,0 +1,20 @@
# Worker multi-stage build (spec §5051)
FROM node:22-alpine AS builder
WORKDIR /build
RUN corepack enable
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./
COPY tsconfig.base.json turbo.json ./
COPY packages ./packages
COPY apps/worker ./apps/worker
RUN pnpm install --frozen-lockfile --filter @app/worker...
RUN pnpm --filter @app/worker build
FROM node:22-alpine AS runtime
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /build/apps/worker/dist ./dist
COPY --from=builder /build/apps/worker/package.json ./package.json
COPY --from=builder /build/node_modules ./node_modules
USER app
CMD ["node", "dist/index.js"]
@@ -0,0 +1,32 @@
# Lokal utvecklingsmiljö: Postgres + Redis.
# API/worker/admin körs med pnpm dev på värddatorn.
services:
postgres:
image: postgres:16-alpine
container_name: app-postgres-dev
environment:
POSTGRES_DB: app
POSTGRES_USER: app_user
POSTGRES_PASSWORD: app_dev_password
ports:
- "5432:5432"
volumes:
- app_pg_dev:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
container_name: app-redis-dev
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "noeviction"]
ports:
- "6379:6379"
volumes:
- app_redis_dev:/data
volumes:
app_pg_dev:
app_redis_dev:
@@ -0,0 +1,103 @@
# Produktionskomposition för befintlig server (spec §51, steg 1 i skalningsplanen).
# Placeras under /opt/app-platform/. Secrets injiceras via .env-fil som
# ägs av appens dedikerade användare (chmod 600) tills SSM-integration (steg 2).
#
# Krav enligt spec §51: egen Linux-user, egna secrets, egna loggar, egna larm,
# egen domän, egen S3-bucket, egen IAM-roll, CPU/RAM-limits. Se docs/infrastruktur.md.
services:
app-api:
build:
context: ../..
dockerfile: infrastructure/docker/Dockerfile.api
image: app-api:latest
container_name: app-api
env_file: /opt/app-platform/secrets/api.env
ports:
- "127.0.0.1:4100:4000" # bakom befintlig reverse proxy
restart: unless-stopped
depends_on:
app-redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: "1.0"
memory: 768M
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
app-worker:
build:
context: ../..
dockerfile: infrastructure/docker/Dockerfile.worker
image: app-worker:latest
container_name: app-worker
env_file: /opt/app-platform/secrets/worker.env
restart: unless-stopped
depends_on:
app-redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: "1.0"
memory: 768M
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
app-admin:
build:
context: ../..
dockerfile: infrastructure/docker/Dockerfile.admin
args:
VITE_API_BASE_URL: https://api.example.invalid # sätt via brand/.env
image: app-admin:latest
container_name: app-admin
ports:
- "127.0.0.1:4180:80"
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.25"
memory: 128M
app-redis:
image: redis:7-alpine
container_name: app-redis
command:
[
"redis-server",
"--appendonly",
"yes",
"--maxmemory",
"512mb",
"--maxmemory-policy",
"noeviction",
]
volumes:
- app_redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
memory: 640M
# PostgreSQL: befintlig RDS-instans med separat separat databas och
# användaren app-användaren med minsta möjliga privilegier (spec §52).
# Se infrastructure/deployment/create-database.sql.
volumes:
app_redis_data:
+19
View File
@@ -0,0 +1,19 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA-routing
location / {
try_files $uri $uri/ /index.html;
}
# Säkerhetsheaders
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
gzip on;
gzip_types text/css application/javascript application/json;
}
@@ -0,0 +1,1051 @@
CREATE TYPE "public"."activity_level" AS ENUM('sedentary', 'light', 'moderate', 'active', 'very_active');--> statement-breakpoint
CREATE TYPE "public"."allergen" AS ENUM('gluten', 'crustaceans', 'eggs', 'fish', 'peanuts', 'soy', 'milk', 'tree_nuts', 'celery', 'mustard', 'sesame', 'sulphites', 'lupin', 'molluscs');--> statement-breakpoint
CREATE TYPE "public"."consent_kind" AS ENUM('personalization', 'anonymized_improvement', 'image_training', 'health_integration', 'location_weather', 'push_notifications');--> statement-breakpoint
CREATE TYPE "public"."consent_status" AS ENUM('granted', 'denied', 'revoked');--> statement-breakpoint
CREATE TYPE "public"."cooking_method" AS ENUM('stovetop', 'oven', 'grill', 'airfryer', 'wok', 'slow_cooker', 'sous_vide', 'microwave', 'no_cook', 'pressure_cooker', 'deep_fry', 'steam');--> statement-breakpoint
CREATE TYPE "public"."creator_level" AS ENUM('beginner', 'sous_chef', 'chef', 'master_chef', 'legend');--> statement-breakpoint
CREATE TYPE "public"."cuisine" AS ENUM('swedish', 'nordic', 'italian', 'french', 'spanish', 'greek', 'thai', 'chinese', 'japanese', 'korean', 'vietnamese', 'indian', 'mexican', 'american', 'turkish', 'lebanese', 'moroccan', 'middle_eastern', 'international');--> statement-breakpoint
CREATE TYPE "public"."date_kind" AS ENUM('best_before', 'use_by');--> statement-breakpoint
CREATE TYPE "public"."diet_pattern" AS ENUM('omnivore', 'flexitarian', 'pescatarian', 'vegetarian', 'vegan', 'low_carb', 'keto', 'high_protein', 'mediterranean');--> statement-breakpoint
CREATE TYPE "public"."event_type" AS ENUM('PRODUCT_ADDED', 'PRODUCT_UPDATED', 'PRODUCT_CONSUMED', 'PRODUCT_DISCARDED', 'RECIPE_COOKED', 'RECIPE_RATED', 'RECIPE_CREATED', 'RECIPE_FORKED', 'MEAL_LOGGED', 'MEAL_PHOTO_ANALYZED', 'HOUSEHOLD_MEMBER_ADDED', 'SUBSCRIPTION_STARTED', 'SUBSCRIPTION_CHANGED', 'AI_CORRECTED', 'MEMORY_UPDATED', 'SHOPPING_COMPLETED', 'WEEK_PLAN_UPDATED', 'MEAL_BOX_CREATED', 'MEAL_BOX_CONSUMED');--> statement-breakpoint
CREATE TYPE "public"."expiry_status" AS ENUM('fresh', 'use_soon', 'expiring', 'expired', 'unknown');--> statement-breakpoint
CREATE TYPE "public"."goal_type" AS ENUM('lose_weight', 'gain_weight', 'build_muscle', 'maintain_weight', 'more_protein', 'less_fat', 'more_fiber', 'more_variety', 'less_waste', 'lower_cost', 'cook_more');--> statement-breakpoint
CREATE TYPE "public"."household_role" AS ENUM('owner', 'adult', 'member', 'child');--> statement-breakpoint
CREATE TYPE "public"."inventory_source" AS ENUM('fridge_photo', 'freezer_photo', 'pantry_photo', 'ingredient_photo', 'barcode', 'receipt', 'digital_receipt', 'label_photo', 'manual_search', 'free_text', 'voice', 'cooked_recipe', 'connector', 'seed');--> statement-breakpoint
CREATE TYPE "public"."inventory_transaction_type" AS ENUM('purchase', 'consume', 'discard', 'adjust', 'cook_use', 'leftover_created', 'leftover_consumed', 'correction', 'freeze', 'thaw', 'move');--> statement-breakpoint
CREATE TYPE "public"."job_status" AS ENUM('queued', 'running', 'awaiting_confirmation', 'completed', 'failed', 'canceled');--> statement-breakpoint
CREATE TYPE "public"."job_type" AS ENUM('ANALYZE_FRIDGE_IMAGE', 'ANALYZE_PANTRY_IMAGE', 'ANALYZE_MEAL_IMAGE', 'READ_RECEIPT', 'READ_NUTRITION_LABEL', 'READ_EXPIRY_DATE', 'NORMALIZE_PRODUCTS', 'DEDUPLICATE_INVENTORY', 'CALCULATE_NUTRITION', 'GENERATE_RECIPE_OPTIONS', 'RANK_RECIPES', 'UPDATE_USER_MEMORY', 'GENERATE_WEEK_PLAN', 'SEND_EXPIRY_NOTIFICATION', 'VERIFY_SUBSCRIPTION', 'PROCESS_STORE_NOTIFICATION', 'BUILD_TRAINING_SAMPLE', 'RUN_AI_EVALUATION');--> statement-breakpoint
CREATE TYPE "public"."meal_log_source" AS ENUM('cooked_recipe', 'plate_photo', 'barcode', 'product', 'free_text', 'voice', 'previous_meal', 'meal_box', 'manual');--> statement-breakpoint
CREATE TYPE "public"."meal_type" AS ENUM('breakfast', 'lunch', 'dinner', 'snack', 'dessert', 'starter', 'buffet', 'party');--> statement-breakpoint
CREATE TYPE "public"."memory_kind" AS ENUM('structured_fact', 'event', 'semantic', 'profile_summary', 'recipe_memory');--> statement-breakpoint
CREATE TYPE "public"."notification_type" AS ENUM('expiry_warning', 'meal_box_reminder', 'quick_dinner_suggestion', 'holiday_upcoming', 'creator_new_recipe', 'week_plan_change', 'pantry_forecast', 'subscription_status');--> statement-breakpoint
CREATE TYPE "public"."precision_mode" AS ENUM('simple', 'exact');--> statement-breakpoint
CREATE TYPE "public"."profile_visibility" AS ENUM('private', 'friends', 'national', 'global');--> statement-breakpoint
CREATE TYPE "public"."recipe_difficulty" AS ENUM('beginner', 'easy', 'medium', 'advanced', 'expert');--> statement-breakpoint
CREATE TYPE "public"."recipe_similarity_class" AS ENUM('duplicate', 'variant', 'inspired', 'independent');--> statement-breakpoint
CREATE TYPE "public"."recipe_source_type" AS ENUM('own_editorial', 'ai_assisted_reviewed', 'open_license', 'public_domain', 'licensed_database', 'creator_agreement', 'user_generated');--> statement-breakpoint
CREATE TYPE "public"."recipe_status" AS ENUM('draft', 'submitted', 'ai_checked', 'in_moderation', 'published', 'rejected', 'archived');--> statement-breakpoint
CREATE TYPE "public"."recipe_variant_type" AS ENUM('standard', 'high_protein', 'low_calorie', 'low_fat', 'vegetarian', 'vegan', 'gluten_free', 'lactose_free', 'kid_friendly', 'airfryer', 'budget');--> statement-breakpoint
CREATE TYPE "public"."recipe_verification_status" AS ENUM('unverified', 'community', 'verified', 'editorial');--> statement-breakpoint
CREATE TYPE "public"."religious_rule" AS ENUM('none', 'halal', 'kosher', 'hindu_no_beef', 'buddhist_vegetarian');--> statement-breakpoint
CREATE TYPE "public"."scan_type" AS ENUM('fridge', 'freezer', 'pantry', 'ingredients', 'plate', 'receipt', 'barcode', 'expiry_date', 'nutrition_label', 'product_package');--> statement-breakpoint
CREATE TYPE "public"."sex" AS ENUM('female', 'male', 'unspecified');--> statement-breakpoint
CREATE TYPE "public"."signal_origin" AS ENUM('user_stated', 'observed', 'ai_inferred');--> statement-breakpoint
CREATE TYPE "public"."storage_location_type" AS ENUM('fridge', 'freezer', 'pantry', 'garage_freezer', 'wine_fridge', 'cellar', 'meal_box', 'custom');--> statement-breakpoint
CREATE TYPE "public"."subscription_plan" AS ENUM('free', 'household', 'family', 'large_household');--> statement-breakpoint
CREATE TYPE "public"."subscription_provider" AS ENUM('apple', 'google', 'promo', 'none');--> statement-breakpoint
CREATE TYPE "public"."subscription_status" AS ENUM('trial', 'active', 'in_grace', 'on_hold', 'paused', 'canceled', 'expired');--> statement-breakpoint
CREATE TYPE "public"."taste_axis" AS ENUM('spice', 'salt', 'acid', 'creaminess', 'garlic', 'sweetness', 'herbs', 'umami');--> statement-breakpoint
CREATE TYPE "public"."unit" AS ENUM('GRAM', 'KILOGRAM', 'MILLILITER', 'DECILITER', 'LITER', 'TEASPOON', 'TABLESPOON', 'CUP_US', 'FLUID_OUNCE_US', 'OUNCE', 'POUND', 'COUNT', 'PORTION', 'PINCH', 'SLICE', 'CLOVE', 'CAN', 'PACKAGE');--> statement-breakpoint
CREATE TYPE "public"."user_role" AS ENUM('user', 'moderator', 'admin');--> statement-breakpoint
CREATE TYPE "public"."verification_status" AS ENUM('unverified', 'user_verified', 'editorially_verified');--> statement-breakpoint
CREATE TYPE "public"."measurement_system" AS ENUM('METRIC', 'US_CUSTOMARY', 'MIXED');--> statement-breakpoint
CREATE TYPE "public"."temperature_unit" AS ENUM('CELSIUS', 'FAHRENHEIT');--> statement-breakpoint
CREATE TYPE "public"."translation_source" AS ENUM('seed', 'ai', 'human');--> statement-breakpoint
CREATE TYPE "public"."translation_status" AS ENUM('draft_ai', 'in_review', 'published');--> statement-breakpoint
CREATE TABLE "admin_totp" (
"user_id" uuid PRIMARY KEY NOT NULL,
"secret_base32" text NOT NULL,
"enabled_at" timestamp with time zone,
"last_used_step" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "email_verification_tokens" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"token_hash" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"used_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "password_reset_tokens" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"token_hash" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"used_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "refresh_tokens" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"token_hash" text NOT NULL,
"family_id" uuid NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"revoked_at" timestamp with time zone,
"replaced_by_token_id" uuid,
"user_agent" text,
"ip" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_consents" (
"user_id" uuid NOT NULL,
"kind" "consent_kind" NOT NULL,
"status" "consent_status" NOT NULL,
"granted_at" timestamp with time zone,
"revoked_at" timestamp with time zone,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "user_consents_user_id_kind_pk" PRIMARY KEY("user_id","kind")
);
--> statement-breakpoint
CREATE TABLE "user_credentials" (
"user_id" uuid PRIMARY KEY NOT NULL,
"password_hash" text NOT NULL,
"password_updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_health_profiles" (
"user_id" uuid PRIMARY KEY NOT NULL,
"birth_year" integer,
"sex" "sex",
"height_cm" double precision,
"weight_kg" double precision,
"target_weight_kg" double precision,
"activity_level" "activity_level" DEFAULT 'moderate' NOT NULL,
"training_sessions_per_week" integer,
"training_types" text[] DEFAULT '{}' NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_preferences" (
"user_id" uuid PRIMARY KEY NOT NULL,
"primary_goal" "goal_type",
"goals" "goal_type"[] DEFAULT '{}' NOT NULL,
"diet_pattern" "diet_pattern" DEFAULT 'omnivore' NOT NULL,
"religious_rule" "religious_rule" DEFAULT 'none' NOT NULL,
"allergens" "allergen"[] DEFAULT '{}' NOT NULL,
"intolerances" text[] DEFAULT '{}' NOT NULL,
"avoid_ingredient_ids" text[] DEFAULT '{}' NOT NULL,
"favorite_cuisines" "cuisine"[] DEFAULT '{}' NOT NULL,
"disliked_dishes" text[] DEFAULT '{}' NOT NULL,
"spice_level_max" integer DEFAULT 3 NOT NULL,
"weekly_budget_minor" integer,
"max_cooking_minutes_weekday" integer,
"equipment" text[] DEFAULT '{}' NOT NULL,
"default_portions" integer DEFAULT 2 NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" text NOT NULL,
"email_verified_at" timestamp with time zone,
"display_name" text NOT NULL,
"role" "user_role" DEFAULT 'user' NOT NULL,
"locale" text DEFAULT 'sv-SE' NOT NULL,
"precision_mode" "precision_mode" DEFAULT 'simple' NOT NULL,
"onboarding_completed" boolean DEFAULT false NOT NULL,
"deleted_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_locale_preferences" (
"user_id" uuid PRIMARY KEY NOT NULL,
"language_tag" text DEFAULT 'sv-SE' NOT NULL,
"region_code" text DEFAULT 'SE' NOT NULL,
"time_zone" text DEFAULT 'Europe/Stockholm' NOT NULL,
"measurement_system" "measurement_system" DEFAULT 'METRIC' NOT NULL,
"temperature_unit" "temperature_unit" DEFAULT 'CELSIUS' NOT NULL,
"currency_code" text DEFAULT 'SEK' NOT NULL,
"first_day_of_week" integer DEFAULT 1 NOT NULL,
"use_24_hour_time" boolean DEFAULT true NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "household_members" (
"household_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
"role" "household_role" DEFAULT 'member' NOT NULL,
"portion_factor" double precision DEFAULT 1 NOT NULL,
"joined_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "household_members_household_id_user_id_pk" PRIMARY KEY("household_id","user_id")
);
--> statement-breakpoint
CREATE TABLE "households" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"invite_code" text NOT NULL,
"weekly_budget_minor" integer,
"currency_code" varchar(3) DEFAULT 'SEK' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "storage_locations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"type" "storage_location_type" NOT NULL,
"name" text NOT NULL,
"sublocations" text[] DEFAULT '{}' NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "canonical_ingredients" (
"id" text PRIMARY KEY NOT NULL,
"name_sv" text NOT NULL,
"name_en" text NOT NULL,
"aliases" text[] DEFAULT '{}' NOT NULL,
"category" text NOT NULL,
"default_unit" "unit" NOT NULL,
"density_g_per_ml" double precision,
"grams_per_piece" double precision,
"allergens" "allergen"[] DEFAULT '{}' NOT NULL,
"is_vegan" boolean DEFAULT false NOT NULL,
"is_vegetarian" boolean DEFAULT false NOT NULL,
"contains_gluten" boolean DEFAULT false NOT NULL,
"contains_lactose" boolean DEFAULT false NOT NULL,
"is_pork" boolean DEFAULT false NOT NULL,
"is_beef" boolean DEFAULT false NOT NULL,
"is_alcohol" boolean DEFAULT false NOT NULL,
"nutrition_per_100" jsonb NOT NULL,
"nutrition_provenance" jsonb NOT NULL,
"peak_seasons" text[] DEFAULT '{}' NOT NULL,
"shelf_life_guidance" jsonb,
"default_price_minor_per_kg" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "substitutions" (
"id" text PRIMARY KEY NOT NULL,
"from_ingredient_id" text NOT NULL,
"to_ingredient_id" text NOT NULL,
"ratio" double precision DEFAULT 1 NOT NULL,
"instructions_sv" text,
"best_for" text[] DEFAULT '{}' NOT NULL,
"not_recommended_for" text[] DEFAULT '{}' NOT NULL,
"flavor_impact_sv" text,
"texture_impact_sv" text,
"priority" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "products" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"gtin" text,
"name" text NOT NULL,
"brand" text,
"canonical_ingredient_id" text,
"package_size_value" double precision,
"package_size_unit" "unit",
"ingredients_text" text,
"allergens" "allergen"[] DEFAULT '{}' NOT NULL,
"may_contain_allergens" "allergen"[] DEFAULT '{}' NOT NULL,
"nutrition" jsonb,
"image_urls" text[] DEFAULT '{}' NOT NULL,
"language" text DEFAULT 'sv' NOT NULL,
"market" text DEFAULT 'SE' NOT NULL,
"data_source" text NOT NULL,
"verification_status" "verification_status" DEFAULT 'unverified' NOT NULL,
"version" integer DEFAULT 1 NOT NULL,
"valid_from" timestamp with time zone DEFAULT now() NOT NULL,
"valid_to" timestamp with time zone,
"created_by_user_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "inventory_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"canonical_ingredient_id" text,
"product_id" uuid,
"display_name" text NOT NULL,
"brand" text,
"quantity" double precision DEFAULT 0 NOT NULL,
"unit" "unit" NOT NULL,
"storage_location_id" uuid NOT NULL,
"sublocation" text,
"purchased_at" date,
"opened_at" date,
"best_before_date" date,
"use_by_date" date,
"date_kind" date_kind,
"frozen_at" date,
"thawed_at" date,
"price_minor" integer,
"nutrition_per_100" jsonb,
"source" "inventory_source" NOT NULL,
"confidence" double precision DEFAULT 1 NOT NULL,
"verified_by_user" boolean DEFAULT false NOT NULL,
"last_verified_at" timestamp with time zone,
"expiry_status" "expiry_status" DEFAULT 'unknown' NOT NULL,
"depleted_at" timestamp with time zone,
"model_version" text,
"prompt_version" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "inventory_transactions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"inventory_item_id" uuid NOT NULL,
"type" "inventory_transaction_type" NOT NULL,
"quantity_delta" double precision NOT NULL,
"unit" "unit" NOT NULL,
"ref_type" text,
"ref_id" uuid,
"actor_user_id" uuid,
"note" text,
"value_minor" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recipe_cooks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"recipe_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
"household_id" uuid,
"portions_cooked" integer NOT NULL,
"cooked_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recipe_favorites" (
"recipe_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "recipe_favorites_recipe_id_user_id_pk" PRIMARY KEY("recipe_id","user_id")
);
--> statement-breakpoint
CREATE TABLE "recipe_ingredients" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"recipe_id" uuid NOT NULL,
"canonical_ingredient_id" text NOT NULL,
"display_name_sv" text NOT NULL,
"quantity" double precision NOT NULL,
"unit" "unit" NOT NULL,
"note" text,
"optional" boolean DEFAULT false NOT NULL,
"group_name" text,
"sort_order" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recipe_ratings" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"recipe_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
"stars" integer NOT NULL,
"feedback_tags" text[] DEFAULT '{}' NOT NULL,
"comment" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recipe_similarities" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"recipe_a_id" uuid NOT NULL,
"recipe_b_id" uuid NOT NULL,
"similarity_score" double precision NOT NULL,
"classification" "recipe_similarity_class" NOT NULL,
"details" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recipe_source_registry" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source_name" text NOT NULL,
"license" text NOT NULL,
"right_to_store" boolean NOT NULL,
"right_to_modify" boolean NOT NULL,
"right_to_display" boolean NOT NULL,
"attribution_required" boolean DEFAULT false NOT NULL,
"attribution_text" text,
"commercial_use" boolean NOT NULL,
"valid_from" timestamp with time zone DEFAULT now() NOT NULL,
"valid_to" timestamp with time zone,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recipe_steps" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"recipe_id" uuid NOT NULL,
"step_number" integer NOT NULL,
"instruction_sv" text NOT NULL,
"timer_seconds" integer,
"temperature_c" integer,
"tip" text
);
--> statement-breakpoint
CREATE TABLE "recipes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"slug" text NOT NULL,
"title_sv" text NOT NULL,
"description_sv" text DEFAULT '' NOT NULL,
"country" text,
"region" text,
"cuisine" "cuisine" NOT NULL,
"meal_types" "meal_type"[] DEFAULT '{}' NOT NULL,
"tags" text[] DEFAULT '{}' NOT NULL,
"methods" text[] DEFAULT '{}' NOT NULL,
"equipment" text[] DEFAULT '{}' NOT NULL,
"difficulty" "recipe_difficulty" DEFAULT 'easy' NOT NULL,
"prep_time_minutes" integer DEFAULT 0 NOT NULL,
"cook_time_minutes" integer DEFAULT 0 NOT NULL,
"total_time_minutes" integer DEFAULT 0 NOT NULL,
"portions" integer DEFAULT 4 NOT NULL,
"nutrition_per_portion" jsonb NOT NULL,
"allergens" "allergen"[] DEFAULT '{}' NOT NULL,
"spice_level" integer DEFAULT 0 NOT NULL,
"estimated_cost_minor_per_portion" integer,
"storage_guidance_sv" text,
"meal_prep_friendly" boolean DEFAULT false NOT NULL,
"freezer_friendly" boolean DEFAULT false NOT NULL,
"peak_seasons" text[] DEFAULT '{}' NOT NULL,
"holiday_tags" text[] DEFAULT '{}' NOT NULL,
"dna" jsonb NOT NULL,
"variant_type" "recipe_variant_type" DEFAULT 'standard' NOT NULL,
"variant_of_recipe_id" uuid,
"forked_from_recipe_id" uuid,
"status" "recipe_status" DEFAULT 'draft' NOT NULL,
"verification_status" "recipe_verification_status" DEFAULT 'unverified' NOT NULL,
"source_type" "recipe_source_type" NOT NULL,
"source_registry_id" uuid,
"creator_user_id" uuid,
"creator_display_name" text,
"image_urls" text[] DEFAULT '{}' NOT NULL,
"version" integer DEFAULT 1 NOT NULL,
"rating_average" double precision,
"rating_count" integer DEFAULT 0 NOT NULL,
"cook_count" integer DEFAULT 0 NOT NULL,
"favorite_count" integer DEFAULT 0 NOT NULL,
"moderation_note" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ingredient_translations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"ingredient_id" text NOT NULL,
"language_tag" varchar(35) NOT NULL,
"name" text NOT NULL,
"aliases" text[] DEFAULT '{}' NOT NULL,
"source" "translation_source" DEFAULT 'seed' NOT NULL,
"status" "translation_status" DEFAULT 'published' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recipe_step_translations" (
"recipe_id" uuid NOT NULL,
"language_tag" varchar(35) NOT NULL,
"step_number" integer NOT NULL,
"instruction" text NOT NULL,
"tip" text,
CONSTRAINT "recipe_step_translations_recipe_id_language_tag_step_number_pk" PRIMARY KEY("recipe_id","language_tag","step_number")
);
--> statement-breakpoint
CREATE TABLE "recipe_translations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"recipe_id" uuid NOT NULL,
"language_tag" varchar(35) NOT NULL,
"title" text NOT NULL,
"description" text,
"storage_guidance" text,
"status" "translation_status" DEFAULT 'draft_ai' NOT NULL,
"source" "translation_source" DEFAULT 'ai' NOT NULL,
"verification" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "unit_translations" (
"unit_code" "unit" NOT NULL,
"language_tag" varchar(35) NOT NULL,
"abbreviation" text NOT NULL,
"name" text NOT NULL,
CONSTRAINT "unit_translations_unit_code_language_tag_pk" PRIMARY KEY("unit_code","language_tag")
);
--> statement-breakpoint
CREATE TABLE "allergen_market_rules" (
"region_code" varchar(2) NOT NULL,
"allergen" "allergen" NOT NULL,
"must_highlight" boolean DEFAULT true NOT NULL,
CONSTRAINT "allergen_market_rules_region_code_allergen_pk" PRIMARY KEY("region_code","allergen")
);
--> statement-breakpoint
CREATE TABLE "nutrition_display_profiles" (
"region_code" varchar(2) PRIMARY KEY NOT NULL,
"energy_display" text NOT NULL,
"salt_display" text NOT NULL,
"energy_label_key" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "meal_boxes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"recipe_id" uuid,
"title_sv" text NOT NULL,
"portions" integer NOT NULL,
"portions_remaining" integer NOT NULL,
"nutrition_per_portion" jsonb,
"cooked_at" date NOT NULL,
"storage_location_id" uuid NOT NULL,
"frozen" boolean DEFAULT false NOT NULL,
"recommended_use_by" date NOT NULL,
"reserved_for_user_id" uuid,
"status" text DEFAULT 'available' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "meals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"household_id" uuid,
"date" date NOT NULL,
"meal_type" "meal_type" NOT NULL,
"source" "meal_log_source" NOT NULL,
"recipe_id" uuid,
"title_sv" text NOT NULL,
"portion_fraction" double precision DEFAULT 1 NOT NULL,
"nutrition" jsonb NOT NULL,
"nutrition_is_estimate" boolean DEFAULT false NOT NULL,
"estimate_min_kcal" double precision,
"estimate_max_kcal" double precision,
"items" jsonb,
"photo_url" text,
"scan_job_id" uuid,
"logged_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "shopping_list_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"shopping_list_id" uuid NOT NULL,
"canonical_ingredient_id" text,
"display_name" text NOT NULL,
"quantity" double precision DEFAULT 1 NOT NULL,
"unit" "unit" DEFAULT 'COUNT' NOT NULL,
"store_section" text DEFAULT 'hygien_ovrigt' NOT NULL,
"suggested_package_size" text,
"estimated_price_minor" integer,
"checked" boolean DEFAULT false NOT NULL,
"added_by_user_id" uuid,
"origin" text DEFAULT 'manual' NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "shopping_lists" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"name" text DEFAULT 'Inköpslista' NOT NULL,
"status" text DEFAULT 'active' NOT NULL,
"week_plan_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "week_plan_entries" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"week_plan_id" uuid NOT NULL,
"date" date NOT NULL,
"meal_type" "meal_type" NOT NULL,
"recipe_id" uuid,
"meal_box_id" uuid,
"title_sv" text NOT NULL,
"portions" integer DEFAULT 2 NOT NULL,
"status" text DEFAULT 'planned' NOT NULL,
"reschedule_reason_sv" text,
"sort_order" integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "week_plans" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"week_start_date" date NOT NULL,
"status" text DEFAULT 'draft' NOT NULL,
"generated_by" text DEFAULT 'user' NOT NULL,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "price_observations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"canonical_ingredient_id" text,
"product_id" uuid,
"store_name" text,
"price_minor" integer NOT NULL,
"quantity" double precision,
"unit" "unit",
"price_per_kg_minor" integer,
"observed_at" date NOT NULL,
"source" text DEFAULT 'receipt' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "receipt_lines" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"receipt_id" uuid NOT NULL,
"raw_text" text NOT NULL,
"normalized_name" text,
"canonical_ingredient_id" text,
"product_id" uuid,
"quantity" double precision,
"unit" "unit",
"unit_price_minor" integer,
"total_price_minor" integer,
"confidence" double precision DEFAULT 0 NOT NULL,
"verified_by_user" boolean DEFAULT false NOT NULL,
"added_to_inventory" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
CREATE TABLE "receipts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"store_name" text,
"purchase_date" date,
"total_minor" integer,
"discount_minor" integer,
"image_url" text,
"scan_job_id" uuid,
"status" text DEFAULT 'pending' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ai_corrections" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"scan_job_id" uuid,
"user_id" uuid NOT NULL,
"task_type" text NOT NULL,
"ai_output" jsonb NOT NULL,
"user_correction" jsonb NOT NULL,
"model_version" text,
"prompt_version" text,
"consent_snapshot" jsonb NOT NULL,
"exported_to_training" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "scan_jobs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"household_id" uuid,
"scan_type" "scan_type" NOT NULL,
"job_type" "job_type" NOT NULL,
"status" "job_status" DEFAULT 'queued' NOT NULL,
"s3_keys" text[] DEFAULT '{}' NOT NULL,
"context" jsonb,
"result" jsonb,
"error" text,
"model_version" text,
"prompt_version" text,
"latency_ms" integer,
"cost_usd" double precision,
"attempts" integer DEFAULT 0 NOT NULL,
"completed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "food_memories" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid,
"household_id" uuid,
"recipe_id" uuid,
"title_sv" text NOT NULL,
"summary_sv" text NOT NULL,
"holiday_tag" text,
"photo_url" text,
"stars" double precision,
"occurred_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "memory_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid,
"household_id" uuid,
"kind" "memory_kind" NOT NULL,
"key" text NOT NULL,
"summary_sv" text NOT NULL,
"value" jsonb,
"origin" "signal_origin" NOT NULL,
"confidence" double precision DEFAULT 0.5 NOT NULL,
"verified_by_user" boolean DEFAULT false NOT NULL,
"paused" boolean DEFAULT false NOT NULL,
"last_used_at" timestamp with time zone,
"expires_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "taste_signals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"axis" "taste_axis" NOT NULL,
"direction" double precision NOT NULL,
"strength" double precision DEFAULT 0.5 NOT NULL,
"origin" "signal_origin" NOT NULL,
"ref_recipe_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "season_events" (
"id" text PRIMARY KEY NOT NULL,
"slug" text NOT NULL,
"name_sv" text NOT NULL,
"market" text DEFAULT 'SE' NOT NULL,
"date_rule" jsonb NOT NULL,
"lead_days" integer DEFAULT 7 NOT NULL,
"food_tags" text[] DEFAULT '{}' NOT NULL,
"recipe_slugs" text[] DEFAULT '{}' NOT NULL,
"priority" integer DEFAULT 0 NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ai_usage_counters" (
"user_id" uuid NOT NULL,
"month" text NOT NULL,
"ai_scans" integer DEFAULT 0 NOT NULL,
"ai_tokens_in" integer DEFAULT 0 NOT NULL,
"ai_tokens_out" integer DEFAULT 0 NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "store_notifications" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"provider" "subscription_provider" NOT NULL,
"notification_type" text,
"raw_payload" jsonb NOT NULL,
"signature_verified" boolean DEFAULT false NOT NULL,
"processed" boolean DEFAULT false NOT NULL,
"processed_at" timestamp with time zone,
"error" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "store_transactions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"provider" "subscription_provider" NOT NULL,
"transaction_id" text NOT NULL,
"original_transaction_id" text,
"user_id" uuid,
"product_id" text,
"raw_payload" jsonb NOT NULL,
"processed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "subscription_events" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"subscription_id" uuid,
"user_id" uuid,
"event_type" text NOT NULL,
"payload" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "subscriptions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"household_id" uuid,
"provider" "subscription_provider" NOT NULL,
"product_id" text NOT NULL,
"plan" "subscription_plan" NOT NULL,
"original_transaction_id" text,
"status" "subscription_status" NOT NULL,
"purchased_at" timestamp with time zone,
"expires_at" timestamp with time zone,
"grace_period_expires_at" timestamp with time zone,
"canceled_at" timestamp with time zone,
"last_verified_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "trials" (
"user_id" uuid PRIMARY KEY NOT NULL,
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
"ends_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ai_eval_cases" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"task_type" text NOT NULL,
"name_sv" text NOT NULL,
"input_ref" jsonb NOT NULL,
"expected_output" jsonb NOT NULL,
"category" text DEFAULT 'standard' NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ai_eval_runs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"task_type" text NOT NULL,
"model_version" text NOT NULL,
"prompt_version" text NOT NULL,
"metrics" jsonb NOT NULL,
"passed" boolean NOT NULL,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "audit_logs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"actor_user_id" uuid,
"actor_type" text DEFAULT 'user' NOT NULL,
"action" text NOT NULL,
"target_type" text,
"target_id" text,
"metadata" jsonb,
"ip" text,
"correlation_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "creator_follows" (
"follower_user_id" uuid NOT NULL,
"creator_user_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "creator_follows_follower_user_id_creator_user_id_pk" PRIMARY KEY("follower_user_id","creator_user_id")
);
--> statement-breakpoint
CREATE TABLE "creator_stats" (
"user_id" uuid PRIMARY KEY NOT NULL,
"visibility" "profile_visibility" DEFAULT 'private' NOT NULL,
"level" "creator_level" DEFAULT 'beginner' NOT NULL,
"published_recipes" integer DEFAULT 0 NOT NULL,
"followers" integer DEFAULT 0 NOT NULL,
"total_cooks" integer DEFAULT 0 NOT NULL,
"total_favorites" integer DEFAULT 0 NOT NULL,
"average_rating" double precision,
"verified_recipes" integer DEFAULT 0 NOT NULL,
"badges" text[] DEFAULT '{}' NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "domain_events" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"type" "event_type" NOT NULL,
"user_id" uuid,
"household_id" uuid,
"payload" jsonb NOT NULL,
"correlation_id" text,
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
"published_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "feature_flags" (
"key" text PRIMARY KEY NOT NULL,
"enabled" boolean DEFAULT false NOT NULL,
"description_sv" text,
"rollout_percent" integer DEFAULT 100 NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "idempotency_keys" (
"user_id" uuid NOT NULL,
"key" text NOT NULL,
"endpoint" text NOT NULL,
"response_status" integer,
"response_body" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "idempotency_keys_user_id_key_endpoint_pk" PRIMARY KEY("user_id","key","endpoint")
);
--> statement-breakpoint
CREATE TABLE "notifications" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"type" "notification_type" NOT NULL,
"title_sv" text NOT NULL,
"body_sv" text NOT NULL,
"data" jsonb,
"template_key" text,
"variables" jsonb,
"locale" text,
"scheduled_for" timestamp with time zone,
"sent_at" timestamp with time zone,
"read_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "push_tokens" (
"user_id" uuid NOT NULL,
"token" text NOT NULL,
"platform" text NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "push_tokens_user_id_token_pk" PRIMARY KEY("user_id","token")
);
--> statement-breakpoint
CREATE TABLE "waste_summaries" (
"household_id" uuid NOT NULL,
"week_start_date" text NOT NULL,
"discarded_count" integer DEFAULT 0 NOT NULL,
"discarded_value_minor" integer DEFAULT 0 NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "waste_summaries_household_id_week_start_date_pk" PRIMARY KEY("household_id","week_start_date")
);
--> statement-breakpoint
ALTER TABLE "admin_totp" ADD CONSTRAINT "admin_totp_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "email_verification_tokens" ADD CONSTRAINT "email_verification_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_consents" ADD CONSTRAINT "user_consents_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_health_profiles" ADD CONSTRAINT "user_health_profiles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_locale_preferences" ADD CONSTRAINT "user_locale_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "household_members" ADD CONSTRAINT "household_members_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "household_members" ADD CONSTRAINT "household_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "storage_locations" ADD CONSTRAINT "storage_locations_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "substitutions" ADD CONSTRAINT "substitutions_from_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("from_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "substitutions" ADD CONSTRAINT "substitutions_to_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("to_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "products" ADD CONSTRAINT "products_canonical_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("canonical_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_items" ADD CONSTRAINT "inventory_items_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_items" ADD CONSTRAINT "inventory_items_canonical_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("canonical_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_items" ADD CONSTRAINT "inventory_items_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_items" ADD CONSTRAINT "inventory_items_storage_location_id_storage_locations_id_fk" FOREIGN KEY ("storage_location_id") REFERENCES "public"."storage_locations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_transactions" ADD CONSTRAINT "inventory_transactions_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_transactions" ADD CONSTRAINT "inventory_transactions_inventory_item_id_inventory_items_id_fk" FOREIGN KEY ("inventory_item_id") REFERENCES "public"."inventory_items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_transactions" ADD CONSTRAINT "inventory_transactions_actor_user_id_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_cooks" ADD CONSTRAINT "recipe_cooks_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_cooks" ADD CONSTRAINT "recipe_cooks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_favorites" ADD CONSTRAINT "recipe_favorites_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_favorites" ADD CONSTRAINT "recipe_favorites_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_canonical_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("canonical_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_ratings" ADD CONSTRAINT "recipe_ratings_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_ratings" ADD CONSTRAINT "recipe_ratings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_similarities" ADD CONSTRAINT "recipe_similarities_recipe_a_id_recipes_id_fk" FOREIGN KEY ("recipe_a_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_similarities" ADD CONSTRAINT "recipe_similarities_recipe_b_id_recipes_id_fk" FOREIGN KEY ("recipe_b_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_steps" ADD CONSTRAINT "recipe_steps_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipes" ADD CONSTRAINT "recipes_source_registry_id_recipe_source_registry_id_fk" FOREIGN KEY ("source_registry_id") REFERENCES "public"."recipe_source_registry"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipes" ADD CONSTRAINT "recipes_creator_user_id_users_id_fk" FOREIGN KEY ("creator_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ingredient_translations" ADD CONSTRAINT "ingredient_translations_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_step_translations" ADD CONSTRAINT "recipe_step_translations_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "recipe_translations" ADD CONSTRAINT "recipe_translations_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "meal_boxes" ADD CONSTRAINT "meal_boxes_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "meal_boxes" ADD CONSTRAINT "meal_boxes_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "meal_boxes" ADD CONSTRAINT "meal_boxes_storage_location_id_storage_locations_id_fk" FOREIGN KEY ("storage_location_id") REFERENCES "public"."storage_locations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "meal_boxes" ADD CONSTRAINT "meal_boxes_reserved_for_user_id_users_id_fk" FOREIGN KEY ("reserved_for_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "meals" ADD CONSTRAINT "meals_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "meals" ADD CONSTRAINT "meals_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "meals" ADD CONSTRAINT "meals_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_shopping_list_id_shopping_lists_id_fk" FOREIGN KEY ("shopping_list_id") REFERENCES "public"."shopping_lists"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_canonical_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("canonical_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_added_by_user_id_users_id_fk" FOREIGN KEY ("added_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_lists" ADD CONSTRAINT "shopping_lists_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shopping_lists" ADD CONSTRAINT "shopping_lists_week_plan_id_week_plans_id_fk" FOREIGN KEY ("week_plan_id") REFERENCES "public"."week_plans"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "week_plan_entries" ADD CONSTRAINT "week_plan_entries_week_plan_id_week_plans_id_fk" FOREIGN KEY ("week_plan_id") REFERENCES "public"."week_plans"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "week_plan_entries" ADD CONSTRAINT "week_plan_entries_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "week_plan_entries" ADD CONSTRAINT "week_plan_entries_meal_box_id_meal_boxes_id_fk" FOREIGN KEY ("meal_box_id") REFERENCES "public"."meal_boxes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "week_plans" ADD CONSTRAINT "week_plans_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "price_observations" ADD CONSTRAINT "price_observations_canonical_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("canonical_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "price_observations" ADD CONSTRAINT "price_observations_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "receipt_lines" ADD CONSTRAINT "receipt_lines_receipt_id_receipts_id_fk" FOREIGN KEY ("receipt_id") REFERENCES "public"."receipts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "receipt_lines" ADD CONSTRAINT "receipt_lines_canonical_ingredient_id_canonical_ingredients_id_fk" FOREIGN KEY ("canonical_ingredient_id") REFERENCES "public"."canonical_ingredients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "receipt_lines" ADD CONSTRAINT "receipt_lines_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "receipts" ADD CONSTRAINT "receipts_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_corrections" ADD CONSTRAINT "ai_corrections_scan_job_id_scan_jobs_id_fk" FOREIGN KEY ("scan_job_id") REFERENCES "public"."scan_jobs"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_corrections" ADD CONSTRAINT "ai_corrections_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "scan_jobs" ADD CONSTRAINT "scan_jobs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "scan_jobs" ADD CONSTRAINT "scan_jobs_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "food_memories" ADD CONSTRAINT "food_memories_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "food_memories" ADD CONSTRAINT "food_memories_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "food_memories" ADD CONSTRAINT "food_memories_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "memory_items" ADD CONSTRAINT "memory_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "memory_items" ADD CONSTRAINT "memory_items_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "taste_signals" ADD CONSTRAINT "taste_signals_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "taste_signals" ADD CONSTRAINT "taste_signals_ref_recipe_id_recipes_id_fk" FOREIGN KEY ("ref_recipe_id") REFERENCES "public"."recipes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_usage_counters" ADD CONSTRAINT "ai_usage_counters_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "store_transactions" ADD CONSTRAINT "store_transactions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "subscription_events" ADD CONSTRAINT "subscription_events_subscription_id_subscriptions_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."subscriptions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "subscription_events" ADD CONSTRAINT "subscription_events_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "trials" ADD CONSTRAINT "trials_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "creator_follows" ADD CONSTRAINT "creator_follows_follower_user_id_users_id_fk" FOREIGN KEY ("follower_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "creator_follows" ADD CONSTRAINT "creator_follows_creator_user_id_users_id_fk" FOREIGN KEY ("creator_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "creator_stats" ADD CONSTRAINT "creator_stats_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "idempotency_keys" ADD CONSTRAINT "idempotency_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "push_tokens" ADD CONSTRAINT "push_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "waste_summaries" ADD CONSTRAINT "waste_summaries_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "email_verification_tokens_user_idx" ON "email_verification_tokens" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "email_verification_tokens_hash_unique" ON "email_verification_tokens" USING btree ("token_hash");--> statement-breakpoint
CREATE INDEX "password_reset_tokens_user_idx" ON "password_reset_tokens" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "password_reset_tokens_hash_unique" ON "password_reset_tokens" USING btree ("token_hash");--> statement-breakpoint
CREATE INDEX "refresh_tokens_user_idx" ON "refresh_tokens" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "refresh_tokens_hash_unique" ON "refresh_tokens" USING btree ("token_hash");--> statement-breakpoint
CREATE UNIQUE INDEX "users_email_unique" ON "users" USING btree ("email");--> statement-breakpoint
CREATE INDEX "household_members_user_idx" ON "household_members" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "households_invite_code_unique" ON "households" USING btree ("invite_code");--> statement-breakpoint
CREATE INDEX "storage_locations_household_idx" ON "storage_locations" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "canonical_ingredients_category_idx" ON "canonical_ingredients" USING btree ("category");--> statement-breakpoint
CREATE INDEX "substitutions_from_idx" ON "substitutions" USING btree ("from_ingredient_id");--> statement-breakpoint
CREATE UNIQUE INDEX "substitutions_pair_unique" ON "substitutions" USING btree ("from_ingredient_id","to_ingredient_id");--> statement-breakpoint
CREATE UNIQUE INDEX "products_gtin_version_unique" ON "products" USING btree ("gtin","version") WHERE "products"."gtin" IS NOT NULL;--> statement-breakpoint
CREATE INDEX "products_gtin_current_idx" ON "products" USING btree ("gtin") WHERE "products"."valid_to" IS NULL;--> statement-breakpoint
CREATE INDEX "products_canonical_idx" ON "products" USING btree ("canonical_ingredient_id");--> statement-breakpoint
CREATE INDEX "inventory_items_household_idx" ON "inventory_items" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "inventory_items_household_bb_idx" ON "inventory_items" USING btree ("household_id","best_before_date");--> statement-breakpoint
CREATE INDEX "inventory_items_location_idx" ON "inventory_items" USING btree ("storage_location_id");--> statement-breakpoint
CREATE INDEX "inventory_items_canonical_idx" ON "inventory_items" USING btree ("canonical_ingredient_id");--> statement-breakpoint
CREATE INDEX "inventory_tx_item_idx" ON "inventory_transactions" USING btree ("inventory_item_id");--> statement-breakpoint
CREATE INDEX "inventory_tx_household_time_idx" ON "inventory_transactions" USING btree ("household_id","created_at");--> statement-breakpoint
CREATE INDEX "inventory_tx_type_idx" ON "inventory_transactions" USING btree ("type");--> statement-breakpoint
CREATE INDEX "recipe_cooks_recipe_idx" ON "recipe_cooks" USING btree ("recipe_id");--> statement-breakpoint
CREATE INDEX "recipe_cooks_user_idx" ON "recipe_cooks" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "recipe_favorites_user_idx" ON "recipe_favorites" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "recipe_ingredients_recipe_idx" ON "recipe_ingredients" USING btree ("recipe_id");--> statement-breakpoint
CREATE INDEX "recipe_ingredients_canonical_idx" ON "recipe_ingredients" USING btree ("canonical_ingredient_id");--> statement-breakpoint
CREATE UNIQUE INDEX "recipe_ratings_user_recipe_unique" ON "recipe_ratings" USING btree ("recipe_id","user_id");--> statement-breakpoint
CREATE INDEX "recipe_ratings_recipe_idx" ON "recipe_ratings" USING btree ("recipe_id");--> statement-breakpoint
CREATE UNIQUE INDEX "recipe_similarities_pair_unique" ON "recipe_similarities" USING btree ("recipe_a_id","recipe_b_id");--> statement-breakpoint
CREATE INDEX "recipe_steps_recipe_idx" ON "recipe_steps" USING btree ("recipe_id");--> statement-breakpoint
CREATE UNIQUE INDEX "recipe_steps_recipe_step_unique" ON "recipe_steps" USING btree ("recipe_id","step_number");--> statement-breakpoint
CREATE UNIQUE INDEX "recipes_slug_unique" ON "recipes" USING btree ("slug");--> statement-breakpoint
CREATE INDEX "recipes_status_idx" ON "recipes" USING btree ("status");--> statement-breakpoint
CREATE INDEX "recipes_cuisine_idx" ON "recipes" USING btree ("cuisine");--> statement-breakpoint
CREATE INDEX "recipes_variant_of_idx" ON "recipes" USING btree ("variant_of_recipe_id");--> statement-breakpoint
CREATE INDEX "recipes_creator_idx" ON "recipes" USING btree ("creator_user_id");--> statement-breakpoint
CREATE INDEX "recipes_total_time_idx" ON "recipes" USING btree ("total_time_minutes");--> statement-breakpoint
CREATE UNIQUE INDEX "ingredient_translations_unique" ON "ingredient_translations" USING btree ("ingredient_id","language_tag");--> statement-breakpoint
CREATE INDEX "ingredient_translations_lang_idx" ON "ingredient_translations" USING btree ("language_tag");--> statement-breakpoint
CREATE UNIQUE INDEX "recipe_translations_unique" ON "recipe_translations" USING btree ("recipe_id","language_tag");--> statement-breakpoint
CREATE INDEX "recipe_translations_lang_idx" ON "recipe_translations" USING btree ("language_tag","status");--> statement-breakpoint
CREATE INDEX "meal_boxes_household_idx" ON "meal_boxes" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "meal_boxes_use_by_idx" ON "meal_boxes" USING btree ("household_id","recommended_use_by");--> statement-breakpoint
CREATE INDEX "meals_user_date_idx" ON "meals" USING btree ("user_id","date");--> statement-breakpoint
CREATE INDEX "meals_household_idx" ON "meals" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "shopping_list_items_list_idx" ON "shopping_list_items" USING btree ("shopping_list_id");--> statement-breakpoint
CREATE INDEX "shopping_lists_household_idx" ON "shopping_lists" USING btree ("household_id");--> statement-breakpoint
CREATE INDEX "week_plan_entries_plan_idx" ON "week_plan_entries" USING btree ("week_plan_id");--> statement-breakpoint
CREATE INDEX "week_plans_household_week_idx" ON "week_plans" USING btree ("household_id","week_start_date");--> statement-breakpoint
CREATE INDEX "price_observations_ingredient_idx" ON "price_observations" USING btree ("canonical_ingredient_id","observed_at");--> statement-breakpoint
CREATE INDEX "receipt_lines_receipt_idx" ON "receipt_lines" USING btree ("receipt_id");--> statement-breakpoint
CREATE INDEX "receipts_household_idx" ON "receipts" USING btree ("household_id","purchase_date");--> statement-breakpoint
CREATE INDEX "ai_corrections_task_idx" ON "ai_corrections" USING btree ("task_type","created_at");--> statement-breakpoint
CREATE INDEX "scan_jobs_user_idx" ON "scan_jobs" USING btree ("user_id","created_at");--> statement-breakpoint
CREATE INDEX "scan_jobs_status_idx" ON "scan_jobs" USING btree ("status");--> statement-breakpoint
CREATE INDEX "food_memories_household_idx" ON "food_memories" USING btree ("household_id","occurred_at");--> statement-breakpoint
CREATE INDEX "food_memories_holiday_idx" ON "food_memories" USING btree ("holiday_tag");--> statement-breakpoint
CREATE INDEX "memory_items_user_idx" ON "memory_items" USING btree ("user_id","kind");--> statement-breakpoint
CREATE INDEX "memory_items_household_idx" ON "memory_items" USING btree ("household_id","kind");--> statement-breakpoint
CREATE INDEX "memory_items_key_idx" ON "memory_items" USING btree ("key");--> statement-breakpoint
CREATE INDEX "taste_signals_user_idx" ON "taste_signals" USING btree ("user_id","axis");--> statement-breakpoint
CREATE UNIQUE INDEX "season_events_slug_market_unique" ON "season_events" USING btree ("slug","market");--> statement-breakpoint
CREATE UNIQUE INDEX "ai_usage_user_month_unique" ON "ai_usage_counters" USING btree ("user_id","month");--> statement-breakpoint
CREATE INDEX "store_notifications_pending_idx" ON "store_notifications" USING btree ("processed","created_at");--> statement-breakpoint
CREATE UNIQUE INDEX "store_transactions_unique" ON "store_transactions" USING btree ("provider","transaction_id");--> statement-breakpoint
CREATE INDEX "subscription_events_sub_idx" ON "subscription_events" USING btree ("subscription_id","created_at");--> statement-breakpoint
CREATE INDEX "subscriptions_user_idx" ON "subscriptions" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "subscriptions_original_tx_unique" ON "subscriptions" USING btree ("provider","original_transaction_id") WHERE "subscriptions"."original_transaction_id" IS NOT NULL;--> statement-breakpoint
CREATE INDEX "ai_eval_runs_task_idx" ON "ai_eval_runs" USING btree ("task_type","created_at");--> statement-breakpoint
CREATE INDEX "audit_logs_actor_idx" ON "audit_logs" USING btree ("actor_user_id","created_at");--> statement-breakpoint
CREATE INDEX "audit_logs_action_idx" ON "audit_logs" USING btree ("action","created_at");--> statement-breakpoint
CREATE INDEX "domain_events_unpublished_idx" ON "domain_events" USING btree ("published_at","occurred_at");--> statement-breakpoint
CREATE INDEX "domain_events_type_idx" ON "domain_events" USING btree ("type","occurred_at");--> statement-breakpoint
CREATE INDEX "domain_events_household_idx" ON "domain_events" USING btree ("household_id","occurred_at");--> statement-breakpoint
CREATE INDEX "notifications_user_idx" ON "notifications" USING btree ("user_id","created_at");
@@ -0,0 +1,7754 @@
{
"id": "d83177b4-c967-4df0-8c6d-a72c25766aa4",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.admin_totp": {
"name": "admin_totp",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"secret_base32": {
"name": "secret_base32",
"type": "text",
"primaryKey": false,
"notNull": true
},
"enabled_at": {
"name": "enabled_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"last_used_step": {
"name": "last_used_step",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"admin_totp_user_id_users_id_fk": {
"name": "admin_totp_user_id_users_id_fk",
"tableFrom": "admin_totp",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.email_verification_tokens": {
"name": "email_verification_tokens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"used_at": {
"name": "used_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"email_verification_tokens_user_idx": {
"name": "email_verification_tokens_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"email_verification_tokens_hash_unique": {
"name": "email_verification_tokens_hash_unique",
"columns": [
{
"expression": "token_hash",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"email_verification_tokens_user_id_users_id_fk": {
"name": "email_verification_tokens_user_id_users_id_fk",
"tableFrom": "email_verification_tokens",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.password_reset_tokens": {
"name": "password_reset_tokens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"used_at": {
"name": "used_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"password_reset_tokens_user_idx": {
"name": "password_reset_tokens_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"password_reset_tokens_hash_unique": {
"name": "password_reset_tokens_hash_unique",
"columns": [
{
"expression": "token_hash",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"password_reset_tokens_user_id_users_id_fk": {
"name": "password_reset_tokens_user_id_users_id_fk",
"tableFrom": "password_reset_tokens",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.refresh_tokens": {
"name": "refresh_tokens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"family_id": {
"name": "family_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"replaced_by_token_id": {
"name": "replaced_by_token_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ip": {
"name": "ip",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"refresh_tokens_user_idx": {
"name": "refresh_tokens_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"refresh_tokens_hash_unique": {
"name": "refresh_tokens_hash_unique",
"columns": [
{
"expression": "token_hash",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"refresh_tokens_user_id_users_id_fk": {
"name": "refresh_tokens_user_id_users_id_fk",
"tableFrom": "refresh_tokens",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_consents": {
"name": "user_consents",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "consent_kind",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "consent_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"granted_at": {
"name": "granted_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"user_consents_user_id_users_id_fk": {
"name": "user_consents_user_id_users_id_fk",
"tableFrom": "user_consents",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_consents_user_id_kind_pk": {
"name": "user_consents_user_id_kind_pk",
"columns": ["user_id", "kind"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_credentials": {
"name": "user_credentials",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"password_updated_at": {
"name": "password_updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"user_credentials_user_id_users_id_fk": {
"name": "user_credentials_user_id_users_id_fk",
"tableFrom": "user_credentials",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_health_profiles": {
"name": "user_health_profiles",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"birth_year": {
"name": "birth_year",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"sex": {
"name": "sex",
"type": "sex",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"height_cm": {
"name": "height_cm",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"weight_kg": {
"name": "weight_kg",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"target_weight_kg": {
"name": "target_weight_kg",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"activity_level": {
"name": "activity_level",
"type": "activity_level",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'moderate'"
},
"training_sessions_per_week": {
"name": "training_sessions_per_week",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"training_types": {
"name": "training_types",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"user_health_profiles_user_id_users_id_fk": {
"name": "user_health_profiles_user_id_users_id_fk",
"tableFrom": "user_health_profiles",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_preferences": {
"name": "user_preferences",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"primary_goal": {
"name": "primary_goal",
"type": "goal_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"goals": {
"name": "goals",
"type": "goal_type[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"diet_pattern": {
"name": "diet_pattern",
"type": "diet_pattern",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'omnivore'"
},
"religious_rule": {
"name": "religious_rule",
"type": "religious_rule",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'none'"
},
"allergens": {
"name": "allergens",
"type": "allergen[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"intolerances": {
"name": "intolerances",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"avoid_ingredient_ids": {
"name": "avoid_ingredient_ids",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"favorite_cuisines": {
"name": "favorite_cuisines",
"type": "cuisine[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"disliked_dishes": {
"name": "disliked_dishes",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"spice_level_max": {
"name": "spice_level_max",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 3
},
"weekly_budget_minor": {
"name": "weekly_budget_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"max_cooking_minutes_weekday": {
"name": "max_cooking_minutes_weekday",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"equipment": {
"name": "equipment",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"default_portions": {
"name": "default_portions",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 2
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"user_preferences_user_id_users_id_fk": {
"name": "user_preferences_user_id_users_id_fk",
"tableFrom": "user_preferences",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified_at": {
"name": "email_verified_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "user_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'user'"
},
"locale": {
"name": "locale",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'sv-SE'"
},
"precision_mode": {
"name": "precision_mode",
"type": "precision_mode",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'simple'"
},
"onboarding_completed": {
"name": "onboarding_completed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_locale_preferences": {
"name": "user_locale_preferences",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"language_tag": {
"name": "language_tag",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'sv-SE'"
},
"region_code": {
"name": "region_code",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'SE'"
},
"time_zone": {
"name": "time_zone",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'Europe/Stockholm'"
},
"measurement_system": {
"name": "measurement_system",
"type": "measurement_system",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'METRIC'"
},
"temperature_unit": {
"name": "temperature_unit",
"type": "temperature_unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'CELSIUS'"
},
"currency_code": {
"name": "currency_code",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'SEK'"
},
"first_day_of_week": {
"name": "first_day_of_week",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"use_24_hour_time": {
"name": "use_24_hour_time",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"user_locale_preferences_user_id_users_id_fk": {
"name": "user_locale_preferences_user_id_users_id_fk",
"tableFrom": "user_locale_preferences",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.household_members": {
"name": "household_members",
"schema": "",
"columns": {
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "household_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'member'"
},
"portion_factor": {
"name": "portion_factor",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 1
},
"joined_at": {
"name": "joined_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"household_members_user_idx": {
"name": "household_members_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"household_members_household_id_households_id_fk": {
"name": "household_members_household_id_households_id_fk",
"tableFrom": "household_members",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"household_members_user_id_users_id_fk": {
"name": "household_members_user_id_users_id_fk",
"tableFrom": "household_members",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"household_members_household_id_user_id_pk": {
"name": "household_members_household_id_user_id_pk",
"columns": ["household_id", "user_id"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.households": {
"name": "households",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"invite_code": {
"name": "invite_code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"weekly_budget_minor": {
"name": "weekly_budget_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"currency_code": {
"name": "currency_code",
"type": "varchar(3)",
"primaryKey": false,
"notNull": true,
"default": "'SEK'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"households_invite_code_unique": {
"name": "households_invite_code_unique",
"columns": [
{
"expression": "invite_code",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.storage_locations": {
"name": "storage_locations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "storage_location_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"sublocations": {
"name": "sublocations",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"storage_locations_household_idx": {
"name": "storage_locations_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"storage_locations_household_id_households_id_fk": {
"name": "storage_locations_household_id_households_id_fk",
"tableFrom": "storage_locations",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.canonical_ingredients": {
"name": "canonical_ingredients",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name_sv": {
"name": "name_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name_en": {
"name": "name_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"aliases": {
"name": "aliases",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": true
},
"default_unit": {
"name": "default_unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"density_g_per_ml": {
"name": "density_g_per_ml",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"grams_per_piece": {
"name": "grams_per_piece",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"allergens": {
"name": "allergens",
"type": "allergen[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"is_vegan": {
"name": "is_vegan",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"is_vegetarian": {
"name": "is_vegetarian",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"contains_gluten": {
"name": "contains_gluten",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"contains_lactose": {
"name": "contains_lactose",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"is_pork": {
"name": "is_pork",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"is_beef": {
"name": "is_beef",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"is_alcohol": {
"name": "is_alcohol",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"nutrition_per_100": {
"name": "nutrition_per_100",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"nutrition_provenance": {
"name": "nutrition_provenance",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"peak_seasons": {
"name": "peak_seasons",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"shelf_life_guidance": {
"name": "shelf_life_guidance",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"default_price_minor_per_kg": {
"name": "default_price_minor_per_kg",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"canonical_ingredients_category_idx": {
"name": "canonical_ingredients_category_idx",
"columns": [
{
"expression": "category",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.substitutions": {
"name": "substitutions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"from_ingredient_id": {
"name": "from_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"to_ingredient_id": {
"name": "to_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"ratio": {
"name": "ratio",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 1
},
"instructions_sv": {
"name": "instructions_sv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"best_for": {
"name": "best_for",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"not_recommended_for": {
"name": "not_recommended_for",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"flavor_impact_sv": {
"name": "flavor_impact_sv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"texture_impact_sv": {
"name": "texture_impact_sv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"priority": {
"name": "priority",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
}
},
"indexes": {
"substitutions_from_idx": {
"name": "substitutions_from_idx",
"columns": [
{
"expression": "from_ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"substitutions_pair_unique": {
"name": "substitutions_pair_unique",
"columns": [
{
"expression": "from_ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "to_ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"substitutions_from_ingredient_id_canonical_ingredients_id_fk": {
"name": "substitutions_from_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "substitutions",
"tableTo": "canonical_ingredients",
"columnsFrom": ["from_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"substitutions_to_ingredient_id_canonical_ingredients_id_fk": {
"name": "substitutions_to_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "substitutions",
"tableTo": "canonical_ingredients",
"columnsFrom": ["to_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.products": {
"name": "products",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"gtin": {
"name": "gtin",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"brand": {
"name": "brand",
"type": "text",
"primaryKey": false,
"notNull": false
},
"canonical_ingredient_id": {
"name": "canonical_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"package_size_value": {
"name": "package_size_value",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"package_size_unit": {
"name": "package_size_unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"ingredients_text": {
"name": "ingredients_text",
"type": "text",
"primaryKey": false,
"notNull": false
},
"allergens": {
"name": "allergens",
"type": "allergen[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"may_contain_allergens": {
"name": "may_contain_allergens",
"type": "allergen[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"nutrition": {
"name": "nutrition",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"image_urls": {
"name": "image_urls",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'sv'"
},
"market": {
"name": "market",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'SE'"
},
"data_source": {
"name": "data_source",
"type": "text",
"primaryKey": false,
"notNull": true
},
"verification_status": {
"name": "verification_status",
"type": "verification_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'unverified'"
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"valid_from": {
"name": "valid_from",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"valid_to": {
"name": "valid_to",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_by_user_id": {
"name": "created_by_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"products_gtin_version_unique": {
"name": "products_gtin_version_unique",
"columns": [
{
"expression": "gtin",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "version",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"where": "\"products\".\"gtin\" IS NOT NULL",
"concurrently": false,
"method": "btree",
"with": {}
},
"products_gtin_current_idx": {
"name": "products_gtin_current_idx",
"columns": [
{
"expression": "gtin",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"where": "\"products\".\"valid_to\" IS NULL",
"concurrently": false,
"method": "btree",
"with": {}
},
"products_canonical_idx": {
"name": "products_canonical_idx",
"columns": [
{
"expression": "canonical_ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"products_canonical_ingredient_id_canonical_ingredients_id_fk": {
"name": "products_canonical_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "products",
"tableTo": "canonical_ingredients",
"columnsFrom": ["canonical_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.inventory_items": {
"name": "inventory_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"canonical_ingredient_id": {
"name": "canonical_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"product_id": {
"name": "product_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"brand": {
"name": "brand",
"type": "text",
"primaryKey": false,
"notNull": false
},
"quantity": {
"name": "quantity",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 0
},
"unit": {
"name": "unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"storage_location_id": {
"name": "storage_location_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"sublocation": {
"name": "sublocation",
"type": "text",
"primaryKey": false,
"notNull": false
},
"purchased_at": {
"name": "purchased_at",
"type": "date",
"primaryKey": false,
"notNull": false
},
"opened_at": {
"name": "opened_at",
"type": "date",
"primaryKey": false,
"notNull": false
},
"best_before_date": {
"name": "best_before_date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"use_by_date": {
"name": "use_by_date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"date_kind": {
"name": "date_kind",
"type": "date_kind",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"frozen_at": {
"name": "frozen_at",
"type": "date",
"primaryKey": false,
"notNull": false
},
"thawed_at": {
"name": "thawed_at",
"type": "date",
"primaryKey": false,
"notNull": false
},
"price_minor": {
"name": "price_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"nutrition_per_100": {
"name": "nutrition_per_100",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"source": {
"name": "source",
"type": "inventory_source",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"confidence": {
"name": "confidence",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 1
},
"verified_by_user": {
"name": "verified_by_user",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"last_verified_at": {
"name": "last_verified_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"expiry_status": {
"name": "expiry_status",
"type": "expiry_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'unknown'"
},
"depleted_at": {
"name": "depleted_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"model_version": {
"name": "model_version",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prompt_version": {
"name": "prompt_version",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"inventory_items_household_idx": {
"name": "inventory_items_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"inventory_items_household_bb_idx": {
"name": "inventory_items_household_bb_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "best_before_date",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"inventory_items_location_idx": {
"name": "inventory_items_location_idx",
"columns": [
{
"expression": "storage_location_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"inventory_items_canonical_idx": {
"name": "inventory_items_canonical_idx",
"columns": [
{
"expression": "canonical_ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"inventory_items_household_id_households_id_fk": {
"name": "inventory_items_household_id_households_id_fk",
"tableFrom": "inventory_items",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"inventory_items_canonical_ingredient_id_canonical_ingredients_id_fk": {
"name": "inventory_items_canonical_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "inventory_items",
"tableTo": "canonical_ingredients",
"columnsFrom": ["canonical_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"inventory_items_product_id_products_id_fk": {
"name": "inventory_items_product_id_products_id_fk",
"tableFrom": "inventory_items",
"tableTo": "products",
"columnsFrom": ["product_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"inventory_items_storage_location_id_storage_locations_id_fk": {
"name": "inventory_items_storage_location_id_storage_locations_id_fk",
"tableFrom": "inventory_items",
"tableTo": "storage_locations",
"columnsFrom": ["storage_location_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.inventory_transactions": {
"name": "inventory_transactions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"inventory_item_id": {
"name": "inventory_item_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "inventory_transaction_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"quantity_delta": {
"name": "quantity_delta",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"unit": {
"name": "unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"ref_type": {
"name": "ref_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ref_id": {
"name": "ref_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"actor_user_id": {
"name": "actor_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false
},
"value_minor": {
"name": "value_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"inventory_tx_item_idx": {
"name": "inventory_tx_item_idx",
"columns": [
{
"expression": "inventory_item_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"inventory_tx_household_time_idx": {
"name": "inventory_tx_household_time_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"inventory_tx_type_idx": {
"name": "inventory_tx_type_idx",
"columns": [
{
"expression": "type",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"inventory_transactions_household_id_households_id_fk": {
"name": "inventory_transactions_household_id_households_id_fk",
"tableFrom": "inventory_transactions",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"inventory_transactions_inventory_item_id_inventory_items_id_fk": {
"name": "inventory_transactions_inventory_item_id_inventory_items_id_fk",
"tableFrom": "inventory_transactions",
"tableTo": "inventory_items",
"columnsFrom": ["inventory_item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"inventory_transactions_actor_user_id_users_id_fk": {
"name": "inventory_transactions_actor_user_id_users_id_fk",
"tableFrom": "inventory_transactions",
"tableTo": "users",
"columnsFrom": ["actor_user_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_cooks": {
"name": "recipe_cooks",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"portions_cooked": {
"name": "portions_cooked",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"cooked_at": {
"name": "cooked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"recipe_cooks_recipe_idx": {
"name": "recipe_cooks_recipe_idx",
"columns": [
{
"expression": "recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipe_cooks_user_idx": {
"name": "recipe_cooks_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipe_cooks_recipe_id_recipes_id_fk": {
"name": "recipe_cooks_recipe_id_recipes_id_fk",
"tableFrom": "recipe_cooks",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"recipe_cooks_user_id_users_id_fk": {
"name": "recipe_cooks_user_id_users_id_fk",
"tableFrom": "recipe_cooks",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_favorites": {
"name": "recipe_favorites",
"schema": "",
"columns": {
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"recipe_favorites_user_idx": {
"name": "recipe_favorites_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipe_favorites_recipe_id_recipes_id_fk": {
"name": "recipe_favorites_recipe_id_recipes_id_fk",
"tableFrom": "recipe_favorites",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"recipe_favorites_user_id_users_id_fk": {
"name": "recipe_favorites_user_id_users_id_fk",
"tableFrom": "recipe_favorites",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"recipe_favorites_recipe_id_user_id_pk": {
"name": "recipe_favorites_recipe_id_user_id_pk",
"columns": ["recipe_id", "user_id"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_ingredients": {
"name": "recipe_ingredients",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"canonical_ingredient_id": {
"name": "canonical_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"display_name_sv": {
"name": "display_name_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"quantity": {
"name": "quantity",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"unit": {
"name": "unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false
},
"optional": {
"name": "optional",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"group_name": {
"name": "group_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
}
},
"indexes": {
"recipe_ingredients_recipe_idx": {
"name": "recipe_ingredients_recipe_idx",
"columns": [
{
"expression": "recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipe_ingredients_canonical_idx": {
"name": "recipe_ingredients_canonical_idx",
"columns": [
{
"expression": "canonical_ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipe_ingredients_recipe_id_recipes_id_fk": {
"name": "recipe_ingredients_recipe_id_recipes_id_fk",
"tableFrom": "recipe_ingredients",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"recipe_ingredients_canonical_ingredient_id_canonical_ingredients_id_fk": {
"name": "recipe_ingredients_canonical_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "recipe_ingredients",
"tableTo": "canonical_ingredients",
"columnsFrom": ["canonical_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_ratings": {
"name": "recipe_ratings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stars": {
"name": "stars",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"feedback_tags": {
"name": "feedback_tags",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"recipe_ratings_user_recipe_unique": {
"name": "recipe_ratings_user_recipe_unique",
"columns": [
{
"expression": "recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipe_ratings_recipe_idx": {
"name": "recipe_ratings_recipe_idx",
"columns": [
{
"expression": "recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipe_ratings_recipe_id_recipes_id_fk": {
"name": "recipe_ratings_recipe_id_recipes_id_fk",
"tableFrom": "recipe_ratings",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"recipe_ratings_user_id_users_id_fk": {
"name": "recipe_ratings_user_id_users_id_fk",
"tableFrom": "recipe_ratings",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_similarities": {
"name": "recipe_similarities",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"recipe_a_id": {
"name": "recipe_a_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"recipe_b_id": {
"name": "recipe_b_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"similarity_score": {
"name": "similarity_score",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"classification": {
"name": "classification",
"type": "recipe_similarity_class",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"details": {
"name": "details",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"recipe_similarities_pair_unique": {
"name": "recipe_similarities_pair_unique",
"columns": [
{
"expression": "recipe_a_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "recipe_b_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipe_similarities_recipe_a_id_recipes_id_fk": {
"name": "recipe_similarities_recipe_a_id_recipes_id_fk",
"tableFrom": "recipe_similarities",
"tableTo": "recipes",
"columnsFrom": ["recipe_a_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"recipe_similarities_recipe_b_id_recipes_id_fk": {
"name": "recipe_similarities_recipe_b_id_recipes_id_fk",
"tableFrom": "recipe_similarities",
"tableTo": "recipes",
"columnsFrom": ["recipe_b_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_source_registry": {
"name": "recipe_source_registry",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"source_name": {
"name": "source_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"license": {
"name": "license",
"type": "text",
"primaryKey": false,
"notNull": true
},
"right_to_store": {
"name": "right_to_store",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"right_to_modify": {
"name": "right_to_modify",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"right_to_display": {
"name": "right_to_display",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"attribution_required": {
"name": "attribution_required",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"attribution_text": {
"name": "attribution_text",
"type": "text",
"primaryKey": false,
"notNull": false
},
"commercial_use": {
"name": "commercial_use",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"valid_from": {
"name": "valid_from",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"valid_to": {
"name": "valid_to",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_steps": {
"name": "recipe_steps",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"step_number": {
"name": "step_number",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"instruction_sv": {
"name": "instruction_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timer_seconds": {
"name": "timer_seconds",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"temperature_c": {
"name": "temperature_c",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"tip": {
"name": "tip",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"recipe_steps_recipe_idx": {
"name": "recipe_steps_recipe_idx",
"columns": [
{
"expression": "recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipe_steps_recipe_step_unique": {
"name": "recipe_steps_recipe_step_unique",
"columns": [
{
"expression": "recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "step_number",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipe_steps_recipe_id_recipes_id_fk": {
"name": "recipe_steps_recipe_id_recipes_id_fk",
"tableFrom": "recipe_steps",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipes": {
"name": "recipes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"title_sv": {
"name": "title_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description_sv": {
"name": "description_sv",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": false
},
"region": {
"name": "region",
"type": "text",
"primaryKey": false,
"notNull": false
},
"cuisine": {
"name": "cuisine",
"type": "cuisine",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"meal_types": {
"name": "meal_types",
"type": "meal_type[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"tags": {
"name": "tags",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"methods": {
"name": "methods",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"equipment": {
"name": "equipment",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"difficulty": {
"name": "difficulty",
"type": "recipe_difficulty",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'easy'"
},
"prep_time_minutes": {
"name": "prep_time_minutes",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"cook_time_minutes": {
"name": "cook_time_minutes",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"total_time_minutes": {
"name": "total_time_minutes",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"portions": {
"name": "portions",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 4
},
"nutrition_per_portion": {
"name": "nutrition_per_portion",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"allergens": {
"name": "allergens",
"type": "allergen[]",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"spice_level": {
"name": "spice_level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"estimated_cost_minor_per_portion": {
"name": "estimated_cost_minor_per_portion",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"storage_guidance_sv": {
"name": "storage_guidance_sv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"meal_prep_friendly": {
"name": "meal_prep_friendly",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"freezer_friendly": {
"name": "freezer_friendly",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"peak_seasons": {
"name": "peak_seasons",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"holiday_tags": {
"name": "holiday_tags",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"dna": {
"name": "dna",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"variant_type": {
"name": "variant_type",
"type": "recipe_variant_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'standard'"
},
"variant_of_recipe_id": {
"name": "variant_of_recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"forked_from_recipe_id": {
"name": "forked_from_recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "recipe_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'draft'"
},
"verification_status": {
"name": "verification_status",
"type": "recipe_verification_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'unverified'"
},
"source_type": {
"name": "source_type",
"type": "recipe_source_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"source_registry_id": {
"name": "source_registry_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"creator_user_id": {
"name": "creator_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"creator_display_name": {
"name": "creator_display_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"image_urls": {
"name": "image_urls",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"rating_average": {
"name": "rating_average",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"rating_count": {
"name": "rating_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"cook_count": {
"name": "cook_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"favorite_count": {
"name": "favorite_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"moderation_note": {
"name": "moderation_note",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"recipes_slug_unique": {
"name": "recipes_slug_unique",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipes_status_idx": {
"name": "recipes_status_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipes_cuisine_idx": {
"name": "recipes_cuisine_idx",
"columns": [
{
"expression": "cuisine",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipes_variant_of_idx": {
"name": "recipes_variant_of_idx",
"columns": [
{
"expression": "variant_of_recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipes_creator_idx": {
"name": "recipes_creator_idx",
"columns": [
{
"expression": "creator_user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipes_total_time_idx": {
"name": "recipes_total_time_idx",
"columns": [
{
"expression": "total_time_minutes",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipes_source_registry_id_recipe_source_registry_id_fk": {
"name": "recipes_source_registry_id_recipe_source_registry_id_fk",
"tableFrom": "recipes",
"tableTo": "recipe_source_registry",
"columnsFrom": ["source_registry_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"recipes_creator_user_id_users_id_fk": {
"name": "recipes_creator_user_id_users_id_fk",
"tableFrom": "recipes",
"tableTo": "users",
"columnsFrom": ["creator_user_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ingredient_translations": {
"name": "ingredient_translations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"ingredient_id": {
"name": "ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"language_tag": {
"name": "language_tag",
"type": "varchar(35)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"aliases": {
"name": "aliases",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"source": {
"name": "source",
"type": "translation_source",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'seed'"
},
"status": {
"name": "status",
"type": "translation_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'published'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"ingredient_translations_unique": {
"name": "ingredient_translations_unique",
"columns": [
{
"expression": "ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "language_tag",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"ingredient_translations_lang_idx": {
"name": "ingredient_translations_lang_idx",
"columns": [
{
"expression": "language_tag",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"ingredient_translations_ingredient_id_canonical_ingredients_id_fk": {
"name": "ingredient_translations_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "ingredient_translations",
"tableTo": "canonical_ingredients",
"columnsFrom": ["ingredient_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_step_translations": {
"name": "recipe_step_translations",
"schema": "",
"columns": {
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"language_tag": {
"name": "language_tag",
"type": "varchar(35)",
"primaryKey": false,
"notNull": true
},
"step_number": {
"name": "step_number",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"instruction": {
"name": "instruction",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tip": {
"name": "tip",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"recipe_step_translations_recipe_id_recipes_id_fk": {
"name": "recipe_step_translations_recipe_id_recipes_id_fk",
"tableFrom": "recipe_step_translations",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"recipe_step_translations_recipe_id_language_tag_step_number_pk": {
"name": "recipe_step_translations_recipe_id_language_tag_step_number_pk",
"columns": ["recipe_id", "language_tag", "step_number"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recipe_translations": {
"name": "recipe_translations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"language_tag": {
"name": "language_tag",
"type": "varchar(35)",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"storage_guidance": {
"name": "storage_guidance",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "translation_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'draft_ai'"
},
"source": {
"name": "source",
"type": "translation_source",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'ai'"
},
"verification": {
"name": "verification",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"recipe_translations_unique": {
"name": "recipe_translations_unique",
"columns": [
{
"expression": "recipe_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "language_tag",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"recipe_translations_lang_idx": {
"name": "recipe_translations_lang_idx",
"columns": [
{
"expression": "language_tag",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"recipe_translations_recipe_id_recipes_id_fk": {
"name": "recipe_translations_recipe_id_recipes_id_fk",
"tableFrom": "recipe_translations",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.unit_translations": {
"name": "unit_translations",
"schema": "",
"columns": {
"unit_code": {
"name": "unit_code",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"language_tag": {
"name": "language_tag",
"type": "varchar(35)",
"primaryKey": false,
"notNull": true
},
"abbreviation": {
"name": "abbreviation",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"unit_translations_unit_code_language_tag_pk": {
"name": "unit_translations_unit_code_language_tag_pk",
"columns": ["unit_code", "language_tag"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.allergen_market_rules": {
"name": "allergen_market_rules",
"schema": "",
"columns": {
"region_code": {
"name": "region_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"allergen": {
"name": "allergen",
"type": "allergen",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"must_highlight": {
"name": "must_highlight",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"allergen_market_rules_region_code_allergen_pk": {
"name": "allergen_market_rules_region_code_allergen_pk",
"columns": ["region_code", "allergen"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.nutrition_display_profiles": {
"name": "nutrition_display_profiles",
"schema": "",
"columns": {
"region_code": {
"name": "region_code",
"type": "varchar(2)",
"primaryKey": true,
"notNull": true
},
"energy_display": {
"name": "energy_display",
"type": "text",
"primaryKey": false,
"notNull": true
},
"salt_display": {
"name": "salt_display",
"type": "text",
"primaryKey": false,
"notNull": true
},
"energy_label_key": {
"name": "energy_label_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.meal_boxes": {
"name": "meal_boxes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"title_sv": {
"name": "title_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"portions": {
"name": "portions",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"portions_remaining": {
"name": "portions_remaining",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"nutrition_per_portion": {
"name": "nutrition_per_portion",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"cooked_at": {
"name": "cooked_at",
"type": "date",
"primaryKey": false,
"notNull": true
},
"storage_location_id": {
"name": "storage_location_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"frozen": {
"name": "frozen",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"recommended_use_by": {
"name": "recommended_use_by",
"type": "date",
"primaryKey": false,
"notNull": true
},
"reserved_for_user_id": {
"name": "reserved_for_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'available'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"meal_boxes_household_idx": {
"name": "meal_boxes_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"meal_boxes_use_by_idx": {
"name": "meal_boxes_use_by_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "recommended_use_by",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"meal_boxes_household_id_households_id_fk": {
"name": "meal_boxes_household_id_households_id_fk",
"tableFrom": "meal_boxes",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"meal_boxes_recipe_id_recipes_id_fk": {
"name": "meal_boxes_recipe_id_recipes_id_fk",
"tableFrom": "meal_boxes",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
"meal_boxes_storage_location_id_storage_locations_id_fk": {
"name": "meal_boxes_storage_location_id_storage_locations_id_fk",
"tableFrom": "meal_boxes",
"tableTo": "storage_locations",
"columnsFrom": ["storage_location_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"meal_boxes_reserved_for_user_id_users_id_fk": {
"name": "meal_boxes_reserved_for_user_id_users_id_fk",
"tableFrom": "meal_boxes",
"tableTo": "users",
"columnsFrom": ["reserved_for_user_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.meals": {
"name": "meals",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"meal_type": {
"name": "meal_type",
"type": "meal_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"source": {
"name": "source",
"type": "meal_log_source",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"title_sv": {
"name": "title_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"portion_fraction": {
"name": "portion_fraction",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 1
},
"nutrition": {
"name": "nutrition",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"nutrition_is_estimate": {
"name": "nutrition_is_estimate",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"estimate_min_kcal": {
"name": "estimate_min_kcal",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"estimate_max_kcal": {
"name": "estimate_max_kcal",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"items": {
"name": "items",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"photo_url": {
"name": "photo_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scan_job_id": {
"name": "scan_job_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"logged_at": {
"name": "logged_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"meals_user_date_idx": {
"name": "meals_user_date_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "date",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"meals_household_idx": {
"name": "meals_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"meals_user_id_users_id_fk": {
"name": "meals_user_id_users_id_fk",
"tableFrom": "meals",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"meals_household_id_households_id_fk": {
"name": "meals_household_id_households_id_fk",
"tableFrom": "meals",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
"meals_recipe_id_recipes_id_fk": {
"name": "meals_recipe_id_recipes_id_fk",
"tableFrom": "meals",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.shopping_list_items": {
"name": "shopping_list_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"shopping_list_id": {
"name": "shopping_list_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"canonical_ingredient_id": {
"name": "canonical_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"quantity": {
"name": "quantity",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 1
},
"unit": {
"name": "unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'COUNT'"
},
"store_section": {
"name": "store_section",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'hygien_ovrigt'"
},
"suggested_package_size": {
"name": "suggested_package_size",
"type": "text",
"primaryKey": false,
"notNull": false
},
"estimated_price_minor": {
"name": "estimated_price_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"checked": {
"name": "checked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"added_by_user_id": {
"name": "added_by_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'manual'"
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
}
},
"indexes": {
"shopping_list_items_list_idx": {
"name": "shopping_list_items_list_idx",
"columns": [
{
"expression": "shopping_list_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"shopping_list_items_shopping_list_id_shopping_lists_id_fk": {
"name": "shopping_list_items_shopping_list_id_shopping_lists_id_fk",
"tableFrom": "shopping_list_items",
"tableTo": "shopping_lists",
"columnsFrom": ["shopping_list_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"shopping_list_items_canonical_ingredient_id_canonical_ingredients_id_fk": {
"name": "shopping_list_items_canonical_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "shopping_list_items",
"tableTo": "canonical_ingredients",
"columnsFrom": ["canonical_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"shopping_list_items_added_by_user_id_users_id_fk": {
"name": "shopping_list_items_added_by_user_id_users_id_fk",
"tableFrom": "shopping_list_items",
"tableTo": "users",
"columnsFrom": ["added_by_user_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.shopping_lists": {
"name": "shopping_lists",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'Inköpslista'"
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'active'"
},
"week_plan_id": {
"name": "week_plan_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"shopping_lists_household_idx": {
"name": "shopping_lists_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"shopping_lists_household_id_households_id_fk": {
"name": "shopping_lists_household_id_households_id_fk",
"tableFrom": "shopping_lists",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"shopping_lists_week_plan_id_week_plans_id_fk": {
"name": "shopping_lists_week_plan_id_week_plans_id_fk",
"tableFrom": "shopping_lists",
"tableTo": "week_plans",
"columnsFrom": ["week_plan_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.week_plan_entries": {
"name": "week_plan_entries",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"week_plan_id": {
"name": "week_plan_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"meal_type": {
"name": "meal_type",
"type": "meal_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"meal_box_id": {
"name": "meal_box_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"title_sv": {
"name": "title_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"portions": {
"name": "portions",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 2
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'planned'"
},
"reschedule_reason_sv": {
"name": "reschedule_reason_sv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
}
},
"indexes": {
"week_plan_entries_plan_idx": {
"name": "week_plan_entries_plan_idx",
"columns": [
{
"expression": "week_plan_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"week_plan_entries_week_plan_id_week_plans_id_fk": {
"name": "week_plan_entries_week_plan_id_week_plans_id_fk",
"tableFrom": "week_plan_entries",
"tableTo": "week_plans",
"columnsFrom": ["week_plan_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"week_plan_entries_recipe_id_recipes_id_fk": {
"name": "week_plan_entries_recipe_id_recipes_id_fk",
"tableFrom": "week_plan_entries",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
"week_plan_entries_meal_box_id_meal_boxes_id_fk": {
"name": "week_plan_entries_meal_box_id_meal_boxes_id_fk",
"tableFrom": "week_plan_entries",
"tableTo": "meal_boxes",
"columnsFrom": ["meal_box_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.week_plans": {
"name": "week_plans",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"week_start_date": {
"name": "week_start_date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'draft'"
},
"generated_by": {
"name": "generated_by",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'user'"
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"week_plans_household_week_idx": {
"name": "week_plans_household_week_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "week_start_date",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"week_plans_household_id_households_id_fk": {
"name": "week_plans_household_id_households_id_fk",
"tableFrom": "week_plans",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.price_observations": {
"name": "price_observations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"canonical_ingredient_id": {
"name": "canonical_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"product_id": {
"name": "product_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"store_name": {
"name": "store_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"price_minor": {
"name": "price_minor",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"quantity": {
"name": "quantity",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"unit": {
"name": "unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"price_per_kg_minor": {
"name": "price_per_kg_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"observed_at": {
"name": "observed_at",
"type": "date",
"primaryKey": false,
"notNull": true
},
"source": {
"name": "source",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'receipt'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"price_observations_ingredient_idx": {
"name": "price_observations_ingredient_idx",
"columns": [
{
"expression": "canonical_ingredient_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "observed_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"price_observations_canonical_ingredient_id_canonical_ingredients_id_fk": {
"name": "price_observations_canonical_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "price_observations",
"tableTo": "canonical_ingredients",
"columnsFrom": ["canonical_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"price_observations_product_id_products_id_fk": {
"name": "price_observations_product_id_products_id_fk",
"tableFrom": "price_observations",
"tableTo": "products",
"columnsFrom": ["product_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.receipt_lines": {
"name": "receipt_lines",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"receipt_id": {
"name": "receipt_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"raw_text": {
"name": "raw_text",
"type": "text",
"primaryKey": false,
"notNull": true
},
"normalized_name": {
"name": "normalized_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"canonical_ingredient_id": {
"name": "canonical_ingredient_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"product_id": {
"name": "product_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"quantity": {
"name": "quantity",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"unit": {
"name": "unit",
"type": "unit",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"unit_price_minor": {
"name": "unit_price_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"total_price_minor": {
"name": "total_price_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"confidence": {
"name": "confidence",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 0
},
"verified_by_user": {
"name": "verified_by_user",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"added_to_inventory": {
"name": "added_to_inventory",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {
"receipt_lines_receipt_idx": {
"name": "receipt_lines_receipt_idx",
"columns": [
{
"expression": "receipt_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"receipt_lines_receipt_id_receipts_id_fk": {
"name": "receipt_lines_receipt_id_receipts_id_fk",
"tableFrom": "receipt_lines",
"tableTo": "receipts",
"columnsFrom": ["receipt_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"receipt_lines_canonical_ingredient_id_canonical_ingredients_id_fk": {
"name": "receipt_lines_canonical_ingredient_id_canonical_ingredients_id_fk",
"tableFrom": "receipt_lines",
"tableTo": "canonical_ingredients",
"columnsFrom": ["canonical_ingredient_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"receipt_lines_product_id_products_id_fk": {
"name": "receipt_lines_product_id_products_id_fk",
"tableFrom": "receipt_lines",
"tableTo": "products",
"columnsFrom": ["product_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.receipts": {
"name": "receipts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"store_name": {
"name": "store_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"purchase_date": {
"name": "purchase_date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"total_minor": {
"name": "total_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"discount_minor": {
"name": "discount_minor",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"image_url": {
"name": "image_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scan_job_id": {
"name": "scan_job_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"receipts_household_idx": {
"name": "receipts_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "purchase_date",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"receipts_household_id_households_id_fk": {
"name": "receipts_household_id_households_id_fk",
"tableFrom": "receipts",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ai_corrections": {
"name": "ai_corrections",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"scan_job_id": {
"name": "scan_job_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"task_type": {
"name": "task_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"ai_output": {
"name": "ai_output",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"user_correction": {
"name": "user_correction",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"model_version": {
"name": "model_version",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prompt_version": {
"name": "prompt_version",
"type": "text",
"primaryKey": false,
"notNull": false
},
"consent_snapshot": {
"name": "consent_snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"exported_to_training": {
"name": "exported_to_training",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"ai_corrections_task_idx": {
"name": "ai_corrections_task_idx",
"columns": [
{
"expression": "task_type",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"ai_corrections_scan_job_id_scan_jobs_id_fk": {
"name": "ai_corrections_scan_job_id_scan_jobs_id_fk",
"tableFrom": "ai_corrections",
"tableTo": "scan_jobs",
"columnsFrom": ["scan_job_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
"ai_corrections_user_id_users_id_fk": {
"name": "ai_corrections_user_id_users_id_fk",
"tableFrom": "ai_corrections",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.scan_jobs": {
"name": "scan_jobs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"scan_type": {
"name": "scan_type",
"type": "scan_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"job_type": {
"name": "job_type",
"type": "job_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "job_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'queued'"
},
"s3_keys": {
"name": "s3_keys",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"context": {
"name": "context",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"result": {
"name": "result",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"error": {
"name": "error",
"type": "text",
"primaryKey": false,
"notNull": false
},
"model_version": {
"name": "model_version",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prompt_version": {
"name": "prompt_version",
"type": "text",
"primaryKey": false,
"notNull": false
},
"latency_ms": {
"name": "latency_ms",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost_usd": {
"name": "cost_usd",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"completed_at": {
"name": "completed_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"scan_jobs_user_idx": {
"name": "scan_jobs_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"scan_jobs_status_idx": {
"name": "scan_jobs_status_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"scan_jobs_user_id_users_id_fk": {
"name": "scan_jobs_user_id_users_id_fk",
"tableFrom": "scan_jobs",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"scan_jobs_household_id_households_id_fk": {
"name": "scan_jobs_household_id_households_id_fk",
"tableFrom": "scan_jobs",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.food_memories": {
"name": "food_memories",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"recipe_id": {
"name": "recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"title_sv": {
"name": "title_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary_sv": {
"name": "summary_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"holiday_tag": {
"name": "holiday_tag",
"type": "text",
"primaryKey": false,
"notNull": false
},
"photo_url": {
"name": "photo_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"stars": {
"name": "stars",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"occurred_at": {
"name": "occurred_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"food_memories_household_idx": {
"name": "food_memories_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "occurred_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"food_memories_holiday_idx": {
"name": "food_memories_holiday_idx",
"columns": [
{
"expression": "holiday_tag",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"food_memories_user_id_users_id_fk": {
"name": "food_memories_user_id_users_id_fk",
"tableFrom": "food_memories",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"food_memories_household_id_households_id_fk": {
"name": "food_memories_household_id_households_id_fk",
"tableFrom": "food_memories",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"food_memories_recipe_id_recipes_id_fk": {
"name": "food_memories_recipe_id_recipes_id_fk",
"tableFrom": "food_memories",
"tableTo": "recipes",
"columnsFrom": ["recipe_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.memory_items": {
"name": "memory_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"kind": {
"name": "kind",
"type": "memory_kind",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary_sv": {
"name": "summary_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "signal_origin",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"confidence": {
"name": "confidence",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 0.5
},
"verified_by_user": {
"name": "verified_by_user",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"paused": {
"name": "paused",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"memory_items_user_idx": {
"name": "memory_items_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "kind",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"memory_items_household_idx": {
"name": "memory_items_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "kind",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"memory_items_key_idx": {
"name": "memory_items_key_idx",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"memory_items_user_id_users_id_fk": {
"name": "memory_items_user_id_users_id_fk",
"tableFrom": "memory_items",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"memory_items_household_id_households_id_fk": {
"name": "memory_items_household_id_households_id_fk",
"tableFrom": "memory_items",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.taste_signals": {
"name": "taste_signals",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"axis": {
"name": "axis",
"type": "taste_axis",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"direction": {
"name": "direction",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"strength": {
"name": "strength",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 0.5
},
"origin": {
"name": "origin",
"type": "signal_origin",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"ref_recipe_id": {
"name": "ref_recipe_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"taste_signals_user_idx": {
"name": "taste_signals_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "axis",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"taste_signals_user_id_users_id_fk": {
"name": "taste_signals_user_id_users_id_fk",
"tableFrom": "taste_signals",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"taste_signals_ref_recipe_id_recipes_id_fk": {
"name": "taste_signals_ref_recipe_id_recipes_id_fk",
"tableFrom": "taste_signals",
"tableTo": "recipes",
"columnsFrom": ["ref_recipe_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.season_events": {
"name": "season_events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name_sv": {
"name": "name_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"market": {
"name": "market",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'SE'"
},
"date_rule": {
"name": "date_rule",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"lead_days": {
"name": "lead_days",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 7
},
"food_tags": {
"name": "food_tags",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"recipe_slugs": {
"name": "recipe_slugs",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"priority": {
"name": "priority",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"season_events_slug_market_unique": {
"name": "season_events_slug_market_unique",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "market",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ai_usage_counters": {
"name": "ai_usage_counters",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"month": {
"name": "month",
"type": "text",
"primaryKey": false,
"notNull": true
},
"ai_scans": {
"name": "ai_scans",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"ai_tokens_in": {
"name": "ai_tokens_in",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"ai_tokens_out": {
"name": "ai_tokens_out",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"ai_usage_user_month_unique": {
"name": "ai_usage_user_month_unique",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "month",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"ai_usage_counters_user_id_users_id_fk": {
"name": "ai_usage_counters_user_id_users_id_fk",
"tableFrom": "ai_usage_counters",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.store_notifications": {
"name": "store_notifications",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"provider": {
"name": "provider",
"type": "subscription_provider",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"notification_type": {
"name": "notification_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"raw_payload": {
"name": "raw_payload",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"signature_verified": {
"name": "signature_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"processed": {
"name": "processed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"processed_at": {
"name": "processed_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"error": {
"name": "error",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"store_notifications_pending_idx": {
"name": "store_notifications_pending_idx",
"columns": [
{
"expression": "processed",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.store_transactions": {
"name": "store_transactions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"provider": {
"name": "provider",
"type": "subscription_provider",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"transaction_id": {
"name": "transaction_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"original_transaction_id": {
"name": "original_transaction_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"product_id": {
"name": "product_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"raw_payload": {
"name": "raw_payload",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"processed_at": {
"name": "processed_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"store_transactions_unique": {
"name": "store_transactions_unique",
"columns": [
{
"expression": "provider",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "transaction_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"store_transactions_user_id_users_id_fk": {
"name": "store_transactions_user_id_users_id_fk",
"tableFrom": "store_transactions",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription_events": {
"name": "subscription_events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"subscription_id": {
"name": "subscription_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"event_type": {
"name": "event_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"payload": {
"name": "payload",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"subscription_events_sub_idx": {
"name": "subscription_events_sub_idx",
"columns": [
{
"expression": "subscription_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscription_events_subscription_id_subscriptions_id_fk": {
"name": "subscription_events_subscription_id_subscriptions_id_fk",
"tableFrom": "subscription_events",
"tableTo": "subscriptions",
"columnsFrom": ["subscription_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"subscription_events_user_id_users_id_fk": {
"name": "subscription_events_user_id_users_id_fk",
"tableFrom": "subscription_events",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscriptions": {
"name": "subscriptions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"provider": {
"name": "provider",
"type": "subscription_provider",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan": {
"name": "plan",
"type": "subscription_plan",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"original_transaction_id": {
"name": "original_transaction_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "subscription_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"purchased_at": {
"name": "purchased_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"grace_period_expires_at": {
"name": "grace_period_expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"canceled_at": {
"name": "canceled_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"last_verified_at": {
"name": "last_verified_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"subscriptions_user_idx": {
"name": "subscriptions_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"subscriptions_original_tx_unique": {
"name": "subscriptions_original_tx_unique",
"columns": [
{
"expression": "provider",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "original_transaction_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"where": "\"subscriptions\".\"original_transaction_id\" IS NOT NULL",
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscriptions_user_id_users_id_fk": {
"name": "subscriptions_user_id_users_id_fk",
"tableFrom": "subscriptions",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"subscriptions_household_id_households_id_fk": {
"name": "subscriptions_household_id_households_id_fk",
"tableFrom": "subscriptions",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.trials": {
"name": "trials",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"started_at": {
"name": "started_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"ends_at": {
"name": "ends_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"trials_user_id_users_id_fk": {
"name": "trials_user_id_users_id_fk",
"tableFrom": "trials",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ai_eval_cases": {
"name": "ai_eval_cases",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"task_type": {
"name": "task_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name_sv": {
"name": "name_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"input_ref": {
"name": "input_ref",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"expected_output": {
"name": "expected_output",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'standard'"
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ai_eval_runs": {
"name": "ai_eval_runs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"task_type": {
"name": "task_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"model_version": {
"name": "model_version",
"type": "text",
"primaryKey": false,
"notNull": true
},
"prompt_version": {
"name": "prompt_version",
"type": "text",
"primaryKey": false,
"notNull": true
},
"metrics": {
"name": "metrics",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"passed": {
"name": "passed",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"ai_eval_runs_task_idx": {
"name": "ai_eval_runs_task_idx",
"columns": [
{
"expression": "task_type",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.audit_logs": {
"name": "audit_logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"actor_user_id": {
"name": "actor_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"actor_type": {
"name": "actor_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'user'"
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true
},
"target_type": {
"name": "target_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"target_id": {
"name": "target_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"ip": {
"name": "ip",
"type": "text",
"primaryKey": false,
"notNull": false
},
"correlation_id": {
"name": "correlation_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"audit_logs_actor_idx": {
"name": "audit_logs_actor_idx",
"columns": [
{
"expression": "actor_user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"audit_logs_action_idx": {
"name": "audit_logs_action_idx",
"columns": [
{
"expression": "action",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.creator_follows": {
"name": "creator_follows",
"schema": "",
"columns": {
"follower_user_id": {
"name": "follower_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"creator_user_id": {
"name": "creator_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"creator_follows_follower_user_id_users_id_fk": {
"name": "creator_follows_follower_user_id_users_id_fk",
"tableFrom": "creator_follows",
"tableTo": "users",
"columnsFrom": ["follower_user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"creator_follows_creator_user_id_users_id_fk": {
"name": "creator_follows_creator_user_id_users_id_fk",
"tableFrom": "creator_follows",
"tableTo": "users",
"columnsFrom": ["creator_user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"creator_follows_follower_user_id_creator_user_id_pk": {
"name": "creator_follows_follower_user_id_creator_user_id_pk",
"columns": ["follower_user_id", "creator_user_id"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.creator_stats": {
"name": "creator_stats",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"visibility": {
"name": "visibility",
"type": "profile_visibility",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'private'"
},
"level": {
"name": "level",
"type": "creator_level",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'beginner'"
},
"published_recipes": {
"name": "published_recipes",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"followers": {
"name": "followers",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"total_cooks": {
"name": "total_cooks",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"total_favorites": {
"name": "total_favorites",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"average_rating": {
"name": "average_rating",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"verified_recipes": {
"name": "verified_recipes",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"badges": {
"name": "badges",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"creator_stats_user_id_users_id_fk": {
"name": "creator_stats_user_id_users_id_fk",
"tableFrom": "creator_stats",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.domain_events": {
"name": "domain_events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"type": {
"name": "type",
"type": "event_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"payload": {
"name": "payload",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"correlation_id": {
"name": "correlation_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"occurred_at": {
"name": "occurred_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"published_at": {
"name": "published_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"domain_events_unpublished_idx": {
"name": "domain_events_unpublished_idx",
"columns": [
{
"expression": "published_at",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "occurred_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"domain_events_type_idx": {
"name": "domain_events_type_idx",
"columns": [
{
"expression": "type",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "occurred_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"domain_events_household_idx": {
"name": "domain_events_household_idx",
"columns": [
{
"expression": "household_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "occurred_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.feature_flags": {
"name": "feature_flags",
"schema": "",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"description_sv": {
"name": "description_sv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"rollout_percent": {
"name": "rollout_percent",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 100
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.idempotency_keys": {
"name": "idempotency_keys",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"response_status": {
"name": "response_status",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"response_body": {
"name": "response_body",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"idempotency_keys_user_id_users_id_fk": {
"name": "idempotency_keys_user_id_users_id_fk",
"tableFrom": "idempotency_keys",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"idempotency_keys_user_id_key_endpoint_pk": {
"name": "idempotency_keys_user_id_key_endpoint_pk",
"columns": ["user_id", "key", "endpoint"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.notifications": {
"name": "notifications",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "notification_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"title_sv": {
"name": "title_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body_sv": {
"name": "body_sv",
"type": "text",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"template_key": {
"name": "template_key",
"type": "text",
"primaryKey": false,
"notNull": false
},
"variables": {
"name": "variables",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"locale": {
"name": "locale",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scheduled_for": {
"name": "scheduled_for",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"sent_at": {
"name": "sent_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"read_at": {
"name": "read_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"notifications_user_idx": {
"name": "notifications_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"notifications_user_id_users_id_fk": {
"name": "notifications_user_id_users_id_fk",
"tableFrom": "notifications",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.push_tokens": {
"name": "push_tokens",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"platform": {
"name": "platform",
"type": "text",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"push_tokens_user_id_users_id_fk": {
"name": "push_tokens_user_id_users_id_fk",
"tableFrom": "push_tokens",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"push_tokens_user_id_token_pk": {
"name": "push_tokens_user_id_token_pk",
"columns": ["user_id", "token"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.waste_summaries": {
"name": "waste_summaries",
"schema": "",
"columns": {
"household_id": {
"name": "household_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"week_start_date": {
"name": "week_start_date",
"type": "text",
"primaryKey": false,
"notNull": true
},
"discarded_count": {
"name": "discarded_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"discarded_value_minor": {
"name": "discarded_value_minor",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"waste_summaries_household_id_households_id_fk": {
"name": "waste_summaries_household_id_households_id_fk",
"tableFrom": "waste_summaries",
"tableTo": "households",
"columnsFrom": ["household_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"waste_summaries_household_id_week_start_date_pk": {
"name": "waste_summaries_household_id_week_start_date_pk",
"columns": ["household_id", "week_start_date"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.activity_level": {
"name": "activity_level",
"schema": "public",
"values": ["sedentary", "light", "moderate", "active", "very_active"]
},
"public.allergen": {
"name": "allergen",
"schema": "public",
"values": [
"gluten",
"crustaceans",
"eggs",
"fish",
"peanuts",
"soy",
"milk",
"tree_nuts",
"celery",
"mustard",
"sesame",
"sulphites",
"lupin",
"molluscs"
]
},
"public.consent_kind": {
"name": "consent_kind",
"schema": "public",
"values": [
"personalization",
"anonymized_improvement",
"image_training",
"health_integration",
"location_weather",
"push_notifications"
]
},
"public.consent_status": {
"name": "consent_status",
"schema": "public",
"values": ["granted", "denied", "revoked"]
},
"public.cooking_method": {
"name": "cooking_method",
"schema": "public",
"values": [
"stovetop",
"oven",
"grill",
"airfryer",
"wok",
"slow_cooker",
"sous_vide",
"microwave",
"no_cook",
"pressure_cooker",
"deep_fry",
"steam"
]
},
"public.creator_level": {
"name": "creator_level",
"schema": "public",
"values": ["beginner", "sous_chef", "chef", "master_chef", "legend"]
},
"public.cuisine": {
"name": "cuisine",
"schema": "public",
"values": [
"swedish",
"nordic",
"italian",
"french",
"spanish",
"greek",
"thai",
"chinese",
"japanese",
"korean",
"vietnamese",
"indian",
"mexican",
"american",
"turkish",
"lebanese",
"moroccan",
"middle_eastern",
"international"
]
},
"public.date_kind": {
"name": "date_kind",
"schema": "public",
"values": ["best_before", "use_by"]
},
"public.diet_pattern": {
"name": "diet_pattern",
"schema": "public",
"values": [
"omnivore",
"flexitarian",
"pescatarian",
"vegetarian",
"vegan",
"low_carb",
"keto",
"high_protein",
"mediterranean"
]
},
"public.event_type": {
"name": "event_type",
"schema": "public",
"values": [
"PRODUCT_ADDED",
"PRODUCT_UPDATED",
"PRODUCT_CONSUMED",
"PRODUCT_DISCARDED",
"RECIPE_COOKED",
"RECIPE_RATED",
"RECIPE_CREATED",
"RECIPE_FORKED",
"MEAL_LOGGED",
"MEAL_PHOTO_ANALYZED",
"HOUSEHOLD_MEMBER_ADDED",
"SUBSCRIPTION_STARTED",
"SUBSCRIPTION_CHANGED",
"AI_CORRECTED",
"MEMORY_UPDATED",
"SHOPPING_COMPLETED",
"WEEK_PLAN_UPDATED",
"MEAL_BOX_CREATED",
"MEAL_BOX_CONSUMED"
]
},
"public.expiry_status": {
"name": "expiry_status",
"schema": "public",
"values": ["fresh", "use_soon", "expiring", "expired", "unknown"]
},
"public.goal_type": {
"name": "goal_type",
"schema": "public",
"values": [
"lose_weight",
"gain_weight",
"build_muscle",
"maintain_weight",
"more_protein",
"less_fat",
"more_fiber",
"more_variety",
"less_waste",
"lower_cost",
"cook_more"
]
},
"public.household_role": {
"name": "household_role",
"schema": "public",
"values": ["owner", "adult", "member", "child"]
},
"public.inventory_source": {
"name": "inventory_source",
"schema": "public",
"values": [
"fridge_photo",
"freezer_photo",
"pantry_photo",
"ingredient_photo",
"barcode",
"receipt",
"digital_receipt",
"label_photo",
"manual_search",
"free_text",
"voice",
"cooked_recipe",
"connector",
"seed"
]
},
"public.inventory_transaction_type": {
"name": "inventory_transaction_type",
"schema": "public",
"values": [
"purchase",
"consume",
"discard",
"adjust",
"cook_use",
"leftover_created",
"leftover_consumed",
"correction",
"freeze",
"thaw",
"move"
]
},
"public.job_status": {
"name": "job_status",
"schema": "public",
"values": ["queued", "running", "awaiting_confirmation", "completed", "failed", "canceled"]
},
"public.job_type": {
"name": "job_type",
"schema": "public",
"values": [
"ANALYZE_FRIDGE_IMAGE",
"ANALYZE_PANTRY_IMAGE",
"ANALYZE_MEAL_IMAGE",
"READ_RECEIPT",
"READ_NUTRITION_LABEL",
"READ_EXPIRY_DATE",
"NORMALIZE_PRODUCTS",
"DEDUPLICATE_INVENTORY",
"CALCULATE_NUTRITION",
"GENERATE_RECIPE_OPTIONS",
"RANK_RECIPES",
"UPDATE_USER_MEMORY",
"GENERATE_WEEK_PLAN",
"SEND_EXPIRY_NOTIFICATION",
"VERIFY_SUBSCRIPTION",
"PROCESS_STORE_NOTIFICATION",
"BUILD_TRAINING_SAMPLE",
"RUN_AI_EVALUATION"
]
},
"public.meal_log_source": {
"name": "meal_log_source",
"schema": "public",
"values": [
"cooked_recipe",
"plate_photo",
"barcode",
"product",
"free_text",
"voice",
"previous_meal",
"meal_box",
"manual"
]
},
"public.meal_type": {
"name": "meal_type",
"schema": "public",
"values": ["breakfast", "lunch", "dinner", "snack", "dessert", "starter", "buffet", "party"]
},
"public.memory_kind": {
"name": "memory_kind",
"schema": "public",
"values": ["structured_fact", "event", "semantic", "profile_summary", "recipe_memory"]
},
"public.notification_type": {
"name": "notification_type",
"schema": "public",
"values": [
"expiry_warning",
"meal_box_reminder",
"quick_dinner_suggestion",
"holiday_upcoming",
"creator_new_recipe",
"week_plan_change",
"pantry_forecast",
"subscription_status"
]
},
"public.precision_mode": {
"name": "precision_mode",
"schema": "public",
"values": ["simple", "exact"]
},
"public.profile_visibility": {
"name": "profile_visibility",
"schema": "public",
"values": ["private", "friends", "national", "global"]
},
"public.recipe_difficulty": {
"name": "recipe_difficulty",
"schema": "public",
"values": ["beginner", "easy", "medium", "advanced", "expert"]
},
"public.recipe_similarity_class": {
"name": "recipe_similarity_class",
"schema": "public",
"values": ["duplicate", "variant", "inspired", "independent"]
},
"public.recipe_source_type": {
"name": "recipe_source_type",
"schema": "public",
"values": [
"own_editorial",
"ai_assisted_reviewed",
"open_license",
"public_domain",
"licensed_database",
"creator_agreement",
"user_generated"
]
},
"public.recipe_status": {
"name": "recipe_status",
"schema": "public",
"values": [
"draft",
"submitted",
"ai_checked",
"in_moderation",
"published",
"rejected",
"archived"
]
},
"public.recipe_variant_type": {
"name": "recipe_variant_type",
"schema": "public",
"values": [
"standard",
"high_protein",
"low_calorie",
"low_fat",
"vegetarian",
"vegan",
"gluten_free",
"lactose_free",
"kid_friendly",
"airfryer",
"budget"
]
},
"public.recipe_verification_status": {
"name": "recipe_verification_status",
"schema": "public",
"values": ["unverified", "community", "verified", "editorial"]
},
"public.religious_rule": {
"name": "religious_rule",
"schema": "public",
"values": ["none", "halal", "kosher", "hindu_no_beef", "buddhist_vegetarian"]
},
"public.scan_type": {
"name": "scan_type",
"schema": "public",
"values": [
"fridge",
"freezer",
"pantry",
"ingredients",
"plate",
"receipt",
"barcode",
"expiry_date",
"nutrition_label",
"product_package"
]
},
"public.sex": {
"name": "sex",
"schema": "public",
"values": ["female", "male", "unspecified"]
},
"public.signal_origin": {
"name": "signal_origin",
"schema": "public",
"values": ["user_stated", "observed", "ai_inferred"]
},
"public.storage_location_type": {
"name": "storage_location_type",
"schema": "public",
"values": [
"fridge",
"freezer",
"pantry",
"garage_freezer",
"wine_fridge",
"cellar",
"meal_box",
"custom"
]
},
"public.subscription_plan": {
"name": "subscription_plan",
"schema": "public",
"values": ["free", "household", "family", "large_household"]
},
"public.subscription_provider": {
"name": "subscription_provider",
"schema": "public",
"values": ["apple", "google", "promo", "none"]
},
"public.subscription_status": {
"name": "subscription_status",
"schema": "public",
"values": ["trial", "active", "in_grace", "on_hold", "paused", "canceled", "expired"]
},
"public.taste_axis": {
"name": "taste_axis",
"schema": "public",
"values": ["spice", "salt", "acid", "creaminess", "garlic", "sweetness", "herbs", "umami"]
},
"public.unit": {
"name": "unit",
"schema": "public",
"values": [
"GRAM",
"KILOGRAM",
"MILLILITER",
"DECILITER",
"LITER",
"TEASPOON",
"TABLESPOON",
"CUP_US",
"FLUID_OUNCE_US",
"OUNCE",
"POUND",
"COUNT",
"PORTION",
"PINCH",
"SLICE",
"CLOVE",
"CAN",
"PACKAGE"
]
},
"public.user_role": {
"name": "user_role",
"schema": "public",
"values": ["user", "moderator", "admin"]
},
"public.verification_status": {
"name": "verification_status",
"schema": "public",
"values": ["unverified", "user_verified", "editorially_verified"]
},
"public.measurement_system": {
"name": "measurement_system",
"schema": "public",
"values": ["METRIC", "US_CUSTOMARY", "MIXED"]
},
"public.temperature_unit": {
"name": "temperature_unit",
"schema": "public",
"values": ["CELSIUS", "FAHRENHEIT"]
},
"public.translation_source": {
"name": "translation_source",
"schema": "public",
"values": ["seed", "ai", "human"]
},
"public.translation_status": {
"name": "translation_status",
"schema": "public",
"values": ["draft_ai", "in_review", "published"]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1785832532499,
"tag": "0000_misty_human_robot",
"breakpoints": true
}
]
}
+42
View File
@@ -0,0 +1,42 @@
# Monitoring & observability (spec §58)
## Mät- och larmpunkter
| Signal | Källa | Larmtröskel (initial) |
| --------------------- | --------------------------------------------- | ------------------------ |
| API-fel (5xx-andel) | API-loggar (pino) → CloudWatch | > 2 % över 5 min |
| API-latens p95 | reverse proxy-loggar | > 1 500 ms över 10 min |
| Workerfel | worker-loggar (`✗`-rader) | > 5 fel / 10 min |
| Köns längd | `GET /admin/v1/jobs/overview` (queue.waiting) | > 100 väntande i 10 min |
| Outbox-eftersläp | samma endpoint (outboxPending) | > 500 i 15 min |
| DB-anslutningar | RDS CloudWatch | > 80 % av max |
| Redis-minne | Redis INFO | > 80 % av maxmemory |
| AAMOS-hälsa | `GET /admin/v1/system/health` (aamos.ok) | fel i 3 kontroller i rad |
| AI-kostnad | scan_jobs.cost_usd summerat per dag | > budgettak per dag |
| Subscription-webhooks | store_notifications (processed=false ålder) | äldst > 30 min |
| Sync-konflikter | app-telemetri (senare fas) | – |
| Abuse | rate limit-träffar per IP | manuell granskning |
## Correlation ID
Varje request får/vidarebefordrar `x-correlation-id` (spec §58). ID:t följer med i:
API-loggar → jobbdata → AAMOS-anrop (`metadata.correlationId`) → audit logs.
Sök på ID:t i loggarna för att följa ett flöde genom hela systemet.
## Healthchecks
- `GET /healthz` liveness (alltid 200 när processen lever)
- `GET /readyz` readiness (DB-ping; 503 vid fel)
- Docker HEALTHCHECK på API-containern
- `GET /admin/v1/system/health` DB, Redis, AAMOS, connectors (admin-token)
## Steg 1-implementation (befintlig server)
1. CloudWatch Agent läser Docker json-file-loggar under `/var/lib/docker/containers`.
2. Metric filters på `"level":50` (error) och `✗`-rader → larm till befintlig SNS-kanal.
3. Cron-jobb kör `curl /readyz` varje minut → larm efter 3 fel.
## Steg 3 (egen VPC/ECS)
CloudWatch Container Insights + RDS Performance Insights + strukturerade loggar
till CloudWatch Logs Insights. Dashboards definieras som kod i denna katalog.
@@ -0,0 +1,43 @@
# Säkerhets-hardening checklista (spec §51, §56)
## Server (steg 1, befintlig maskin)
- [ ] Dedikerad Linux-användare `<slug>` (från brand.config.json) utan sudo; äger `/opt/<slug>-platform`
- [ ] Secrets i `/opt/<slug>-platform/secrets/*.env`, ägare `<slug>`, chmod 600
- [ ] Docker-containrar kör som non-root (Dockerfiles gör detta)
- [ ] API exponeras endast på 127.0.0.1 bakom reverse proxy med TLS (API-domän ur brand.config.json)
- [ ] Brandvägg: endast 80/443 inåt; Redis/Postgres nås aldrig utifrån
- [ ] Automatiska säkerhetsuppdateringar (unattended-upgrades)
## AWS
- [ ] Egen IAM-roll `<slug>-app` med åtkomst ENDAST till appens S3-bucket
- [ ] S3: block public access, SSE-S3-kryptering, lifecycle (temporary/ 7 dagar, scans/ 90 dagar)
- [ ] RDS: separat databas + användare med minsta privilegier (create-database.sql)
- [ ] Secrets Manager/SSM för produktionshemligheter (steg 2)
## Applikation
- [x] Lösenord: scrypt med OWASP-parametrar
- [x] Refresh-tokens: hashade, roterande, familjeåterkallelse vid återanvändning
- [x] Rate limiting globalt + strikt på auth-endpoints
- [x] Idempotency-nycklar för känsliga POST (tabell finns; aktiveras per route)
- [x] Correlation-ID på alla requests
- [x] Audit log för känsliga åtgärder
- [x] Zod-validering på all indata
- [x] Säkerhetsheaders (nosniff, frame-deny, no-referrer, no-store)
- [x] CORS låst till kända origins
- [x] Hälsodata i egna tabeller, exponeras aldrig via hushålls-endpoints
- [x] Produktion vägrar starta med dev-hemligheter eller AAMOS_MODE=mock
## GDPR (spec §56)
- [x] Export: `GET /v1/me/export`
- [x] Radering: `DELETE /v1/me` (cascade + anonymisering)
- [x] Samtycken: separata, med tidsstämplar och audit
- [x] Minne: transparent, korrigerbart, pausbart, raderbart
- [x] Träningsdata endast med `anonymized_improvement`-samtycke (snapshot per korrigering)
- [x] Retentionspolicy automatiserad: dagligt RUN_RETENTION-jobb (tokens 7 d, refresh 30 d, skanningar/notiser 90 d); S3 lifecycle sätts vid bucket-skapandet (runbook steg 3)
- [x] Backup-restore-test genomfört och protokollfört (skript: infrastructure/deployment/backup-restore-test.sh; senaste protokoll i infrastructure/backups/ GODKÄND, 62 tabeller identiska). Upprepa kvartalsvis
- [ ] Personuppgiftsbiträdesavtal med AWS + AAMOS-drift
- [x] Ingen hälso-/kostdata till reklam policybeslut, inga annons-SDK:er i appen