diff --git a/docs/superpowers/specs/2026-09-02-about-and-blog-design.md b/docs/superpowers/specs/2026-09-02-about-and-blog-design.md
index 4617d1c..e56e13c 100644
--- a/docs/superpowers/specs/2026-09-02-about-and-blog-design.md
+++ b/docs/superpowers/specs/2026-09-02-about-and-blog-design.md
@@ -73,6 +73,8 @@ Applied to About and every post. Recorded here so the voice does not drift.
- State limits before someone else finds them. Every post that presents a
metric says what it does not show.
- No mission statements, no "passionate about", no invented team.
+- No em dashes. One of the clearest tells of machine-written prose, which is
+ the exact problem this work exists to fix.
- Short sentences. The existing code comments in this repo are already written
this way; the prose should match.
diff --git a/nextjs-app/.gitignore b/nextjs-app/.gitignore
index 1aa592e..bb160fa 100644
--- a/nextjs-app/.gitignore
+++ b/nextjs-app/.gitignore
@@ -40,5 +40,3 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
-# Payload generates this from the config on every build
-/payload-types.ts
diff --git a/nextjs-app/__tests__/payload/migrations.test.ts b/nextjs-app/__tests__/payload/migrations.test.ts
new file mode 100644
index 0000000..56182f1
--- /dev/null
+++ b/nextjs-app/__tests__/payload/migrations.test.ts
@@ -0,0 +1,52 @@
+/**
+ * The generated migration is schema-qualified to "payload" throughout but does
+ * not create that schema — `schemaName` says where tables go, it does not
+ * create anything. On staging and production, which have never run it, the
+ * whole migration fails with `schema "payload" does not exist`.
+ *
+ * The CREATE SCHEMA is therefore hand-added, which makes it exactly the kind
+ * of edit a regeneration silently discards. This is the guard.
+ */
+import fs from 'fs';
+import path from 'path';
+
+const DIR = path.join(__dirname, '..', '..', 'migrations');
+
+function migrationFiles() {
+ return fs
+ .readdirSync(DIR)
+ .filter((f) => f.endsWith('.ts') && f !== 'index.ts');
+}
+
+describe('payload migrations', () => {
+ it('ships at least one migration, so a container has tables to find', () => {
+ expect(migrationFiles().length).toBeGreaterThan(0);
+ });
+
+ it('creates the payload schema before creating anything in it', () => {
+ const initial = migrationFiles().find((f) => f.includes('initial'))!;
+ const sql = fs.readFileSync(path.join(DIR, initial), 'utf8');
+
+ expect(sql).toMatch(/CREATE SCHEMA IF NOT EXISTS "payload"/);
+
+ // Ordering matters: the schema must be created before the first object
+ // that lives in it, or the migration fails on its first statement.
+ expect(sql.indexOf('CREATE SCHEMA IF NOT EXISTS "payload"'))
+ .toBeLessThan(sql.indexOf('CREATE TABLE "payload"'));
+ });
+
+ it('creates the tables the app queries on boot', () => {
+ const initial = migrationFiles().find((f) => f.includes('initial'))!;
+ const sql = fs.readFileSync(path.join(DIR, initial), 'utf8');
+ for (const table of ['users', 'posts', '_posts_v', 'media', 'payload_migrations']) {
+ expect(sql).toContain(`CREATE TABLE "payload"."${table}"`);
+ }
+ });
+
+ it('is wired into the adapter, so it runs on server init', () => {
+ const config = fs.readFileSync(
+ path.join(__dirname, '..', '..', 'payload.config.ts'), 'utf8',
+ );
+ expect(config).toMatch(/prodMigrations:\s*migrations/);
+ });
+});
diff --git a/nextjs-app/app/(frontend)/about/page.tsx b/nextjs-app/app/(frontend)/about/page.tsx
index 8dea136..3107e06 100644
--- a/nextjs-app/app/(frontend)/about/page.tsx
+++ b/nextjs-app/app/(frontend)/about/page.tsx
@@ -42,7 +42,7 @@ export default function AboutPage() {
I'm a parent in south-west London. When we started looking at
- primary schools, I found the information I needed was all published —
+ primary schools, I found the information I needed was all published,
and almost impossible to hold in one place.
@@ -68,11 +68,11 @@ export default function AboutPage() {
- What I do have is the problem itself — I'm going through primary
- admissions right now — and a working knowledge of data, which is what
- I do for a living. That combination is enough to take published
- figures and present them honestly. It is not enough to tell you which
- school is right for your child, and this site never tries to.
+ What I do have is the problem itself. I'm going through primary
+ admissions right now, and I work with data for a living. That
+ combination is enough to take published figures and present them
+ honestly. It is not enough to tell you which school is right for your
+ child, and this site never tries to.
Where the numbers come from
@@ -96,9 +96,10 @@ export default function AboutPage() {
A school is not its results. The figures here describe one year group,
on a handful of days, measured in a way that suits national statistics
- rather than your child. A small cohort makes percentages swing wildly
- — in a class of thirty, one pupil is more than three points. Results
- say nothing at all about whether a child will be happy somewhere.
+ rather than your child. A small cohort makes percentages swing wildly.
+ In a class of thirty, one pupil is worth more than three points.
+ Results say nothing at all about whether a child will be happy
+ somewhere.
@@ -113,8 +114,8 @@ export default function AboutPage() {
If something's wrong
- Tell me and I'll fix it. Genuinely — if a figure looks wrong, or
- a page gives a misleading impression of a school, I want to know.
+ Tell me and I'll fix it. If a figure looks wrong, or a page gives
+ a misleading impression of a school, I genuinely want to know.
It's the fastest way this gets better.
diff --git a/nextjs-app/app/(frontend)/blog/[slug]/page.tsx b/nextjs-app/app/(frontend)/blog/[slug]/page.tsx
index 1fe3048..d717e91 100644
--- a/nextjs-app/app/(frontend)/blog/[slug]/page.tsx
+++ b/nextjs-app/app/(frontend)/blog/[slug]/page.tsx
@@ -5,6 +5,7 @@ import { notFound } from 'next/navigation';
import { RichText } from '@payloadcms/richtext-lexical/react';
import type { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react';
import { getCachedPayload } from '@/lib/payload';
+import type { Post, Media } from '@/payload-types';
import { absoluteUrl } from '@/lib/site';
import {
blogPostingJsonLd,
@@ -24,11 +25,15 @@ import styles from './Post.module.css';
*/
export const revalidate = 3600;
-interface HeroImage {
- url?: string | null;
- alt?: string | null;
- width?: number | null;
- height?: number | null;
+/**
+ * heroImage is `number | Media | null`: an id when the query is shallow, the
+ * populated document at depth 1. Both pages query at depth 1, but narrowing
+ * rather than asserting keeps it correct if that ever changes.
+ */
+function heroOf(post: Post): Media | null {
+ return typeof post.heroImage === 'object' && post.heroImage !== null
+ ? post.heroImage
+ : null;
}
/**
@@ -68,12 +73,12 @@ const findPost = cache(async (slug: string) => {
return docs[0] ?? null;
});
-function summarise(post: Record
) {
+function summarise(post: Post) {
return {
- title: String(post.title),
- slug: String(post.slug),
- excerpt: String(post.excerpt),
- publishedAt: String(post.publishedAt),
+ title: post.title,
+ slug: post.slug,
+ excerpt: post.excerpt,
+ publishedAt: post.publishedAt,
};
}
@@ -84,18 +89,18 @@ export async function generateMetadata(
const post = await findPost(slug);
if (!post) return { title: 'Not found' };
- const hero = post.heroImage as HeroImage | null | undefined;
+ const hero = heroOf(post);
return {
- title: String(post.title),
- description: String(post.excerpt),
+ title: post.title,
+ description: post.excerpt,
alternates: { canonical: absoluteUrl(`/blog/${post.slug}`) },
openGraph: {
type: 'article',
- title: String(post.title),
- description: String(post.excerpt),
+ title: post.title,
+ description: post.excerpt,
url: absoluteUrl(`/blog/${post.slug}`),
- publishedTime: String(post.publishedAt),
+ publishedTime: post.publishedAt,
// A post with a hero image shares that; one without falls through to the
// generated share card at app/opengraph-image.tsx.
...(hero?.url ? { images: [{ url: hero.url }] } : {}),
@@ -110,8 +115,8 @@ export default async function PostPage(
const post = await findPost(slug);
if (!post) notFound();
- const summary = summarise(post as Record);
- const hero = post.heroImage as HeroImage | null | undefined;
+ const summary = summarise(post);
+ const hero = heroOf(post);
const jsonLd = {
'@context': 'https://schema.org',
@@ -164,7 +169,7 @@ export default async function PostPage(
)}
-
+
);
diff --git a/nextjs-app/blocks/Callout.ts b/nextjs-app/blocks/Callout.ts
index fd41654..f71f6a2 100644
--- a/nextjs-app/blocks/Callout.ts
+++ b/nextjs-app/blocks/Callout.ts
@@ -16,8 +16,8 @@ export const Callout: Block = {
type: 'select',
defaultValue: 'caveat',
options: [
- { label: 'Caveat — what this does not show', value: 'caveat' },
- { label: 'Note — useful aside', value: 'note' },
+ { label: 'Caveat: what this does not show', value: 'caveat' },
+ { label: 'Note: useful aside', value: 'note' },
],
},
{ name: 'body', type: 'textarea', required: true },
diff --git a/nextjs-app/docs/PUBLISHING.md b/nextjs-app/docs/PUBLISHING.md
index 75a6eb6..f50254b 100644
--- a/nextjs-app/docs/PUBLISHING.md
+++ b/nextjs-app/docs/PUBLISHING.md
@@ -1,12 +1,12 @@
# Publishing to the blog
The blog is Payload CMS, running inside the Next.js app. There is no separate
-service and no second deploy — writing a post is done in the browser and takes
+service and no second deploy. Writing a post is done in the browser and takes
effect on the live site within seconds.
## Signing in
-`https://www.schoolcompare.co.uk/admin` — one account, no registration. If you
+`https://www.schoolcompare.co.uk/admin`, one account, no registration. If you
need the account seeded on a fresh environment, run against the container:
```bash
@@ -24,29 +24,29 @@ password there.
| Field | Notes |
|---|---|
| **Title** | The `` and the browser tab. |
-| **Slug** | The URL segment, in the sidebar. **Never change it after publishing** — it is the canonical URL, and changing it breaks every existing link and discards the page's accumulated search signal. |
+| **Slug** | The URL segment, in the sidebar. **Never change it after publishing.** It is the canonical URL, and changing it breaks every existing link and discards the page's accumulated search signal. |
| **Published at** | The date shown on the post and in the feed. |
| **Excerpt** | Max 200 characters. Shown on the index *and* used as the meta description, so write it as a standalone sentence rather than a teaser. |
| **Hero image** | Optional. Becomes the social share image; without one, the site's generated card is used. |
| **Content** | Rich text. `/` inserts a block. |
-**Save as draft** while you're working — drafts are not public. **Publish** when
+**Save as draft** while you're working; drafts are not public. **Publish** when
it's ready.
### The callout block
One custom block, `Callout`, with two tones:
-- **Caveat** — what a number does *not* show. This is the one that matters: it
+- **Caveat**: what a number does *not* show. This is the one that matters. It
is how a post states a limitation in context rather than burying it in a
closing paragraph.
-- **Note** — a useful aside.
+- **Note**: a useful aside.
### Images
Every image requires alt text; the editor will not let you save without it.
Uploads go to a Docker volume on the host, which is backed up separately from
-Postgres — an image is not reproducible from the pipeline the way school data
+Postgres. An image is not reproducible from the pipeline the way school data
is.
## How publishing reaches the live site
@@ -55,7 +55,7 @@ is.
so a new post appears immediately.
- `/blog/[slug]` is cached after its first request. Publishing or editing fires
a `revalidatePath` from the collection's `afterChange` hook, which drops that
- cached copy — so edits appear immediately too.
+ cached copy, so edits appear immediately too.
If a change doesn't show, it is far more likely the post is still a draft than
that the cache is stale.
@@ -65,7 +65,7 @@ that the cache is stale.
These rules are why the blog exists. A post that ignores them makes the site
read more machine-generated, not less.
-- **First person singular.** "I built", "I found" — never "we provide".
+- **First person singular.** "I built", "I found", never "we provide".
- **Concrete over general.** "When we were looking at schools in Wandsworth"
beats any amount of stated warmth.
- **State limits before someone else finds them.** Every post that presents a
@@ -73,10 +73,14 @@ read more machine-generated, not less.
human wrote it: generated content does not volunteer its own weaknesses.
- **No mission statements, no "passionate about", no invented team.** There is
one person here.
+- **No em dashes.** They are one of the clearest tells of machine-written
+ prose, which is the whole problem this blog exists to fix. A full stop, a
+ colon, a semicolon or a pair of commas does the job and reads as though a
+ person chose it.
- **Short sentences.**
- **Never publish a surname, an employer, or a child's name.** The site's author
is "Tudor". See `/about`.
- **Never invent a figure**, even illustratively. On a site whose whole
proposition is official data, a made-up number attached to a real school is
- the one thing it cannot do — and no illustrative intent survives being
+ the one thing it cannot do, and no illustrative intent survives being
screenshotted.
diff --git a/nextjs-app/migrations/20260902_172826_initial.json b/nextjs-app/migrations/20260902_172826_initial.json
new file mode 100644
index 0000000..5a4d160
--- /dev/null
+++ b/nextjs-app/migrations/20260902_172826_initial.json
@@ -0,0 +1,1566 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "payload.users_sessions": {
+ "name": "users_sessions",
+ "schema": "payload",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "users_sessions_order_idx": {
+ "name": "users_sessions_order_idx",
+ "columns": [
+ {
+ "expression": "_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_sessions_parent_id_idx": {
+ "name": "users_sessions_parent_id_idx",
+ "columns": [
+ {
+ "expression": "_parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "users_sessions_parent_id_fk": {
+ "name": "users_sessions_parent_id_fk",
+ "tableFrom": "users_sessions",
+ "tableTo": "users",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.users": {
+ "name": "users",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Tudor'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reset_password_token": {
+ "name": "reset_password_token",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reset_password_expiration": {
+ "name": "reset_password_expiration",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "salt": {
+ "name": "salt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "login_attempts": {
+ "name": "login_attempts",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "lock_until": {
+ "name": "lock_until",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "users_updated_at_idx": {
+ "name": "users_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_created_at_idx": {
+ "name": "users_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_email_idx": {
+ "name": "users_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.posts": {
+ "name": "posts",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hero_image_id": {
+ "name": "hero_image_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "_status": {
+ "name": "_status",
+ "type": "enum_posts_status",
+ "typeSchema": "payload",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'draft'"
+ }
+ },
+ "indexes": {
+ "posts_slug_idx": {
+ "name": "posts_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_hero_image_idx": {
+ "name": "posts_hero_image_idx",
+ "columns": [
+ {
+ "expression": "hero_image_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_updated_at_idx": {
+ "name": "posts_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_created_at_idx": {
+ "name": "posts_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts__status_idx": {
+ "name": "posts__status_idx",
+ "columns": [
+ {
+ "expression": "_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "posts_hero_image_id_media_id_fk": {
+ "name": "posts_hero_image_id_media_id_fk",
+ "tableFrom": "posts",
+ "tableTo": "media",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "hero_image_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload._posts_v": {
+ "name": "_posts_v",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_title": {
+ "name": "version_title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_slug": {
+ "name": "version_slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_published_at": {
+ "name": "version_published_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_excerpt": {
+ "name": "version_excerpt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_hero_image_id": {
+ "name": "version_hero_image_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_content": {
+ "name": "version_content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_updated_at": {
+ "name": "version_updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version_created_at": {
+ "name": "version_created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version__status": {
+ "name": "version__status",
+ "type": "enum__posts_v_version_status",
+ "typeSchema": "payload",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'draft'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "latest": {
+ "name": "latest",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "_posts_v_parent_idx": {
+ "name": "_posts_v_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_version_version_slug_idx": {
+ "name": "_posts_v_version_version_slug_idx",
+ "columns": [
+ {
+ "expression": "version_slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_version_version_hero_image_idx": {
+ "name": "_posts_v_version_version_hero_image_idx",
+ "columns": [
+ {
+ "expression": "version_hero_image_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_version_version_updated_at_idx": {
+ "name": "_posts_v_version_version_updated_at_idx",
+ "columns": [
+ {
+ "expression": "version_updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_version_version_created_at_idx": {
+ "name": "_posts_v_version_version_created_at_idx",
+ "columns": [
+ {
+ "expression": "version_created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_version_version__status_idx": {
+ "name": "_posts_v_version_version__status_idx",
+ "columns": [
+ {
+ "expression": "version__status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_created_at_idx": {
+ "name": "_posts_v_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_updated_at_idx": {
+ "name": "_posts_v_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "_posts_v_latest_idx": {
+ "name": "_posts_v_latest_idx",
+ "columns": [
+ {
+ "expression": "latest",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "_posts_v_parent_id_posts_id_fk": {
+ "name": "_posts_v_parent_id_posts_id_fk",
+ "tableFrom": "_posts_v",
+ "tableTo": "posts",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "_posts_v_version_hero_image_id_media_id_fk": {
+ "name": "_posts_v_version_hero_image_id_media_id_fk",
+ "tableFrom": "_posts_v",
+ "tableTo": "media",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "version_hero_image_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.media": {
+ "name": "media",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "alt": {
+ "name": "alt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "url": {
+ "name": "url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "thumbnail_u_r_l": {
+ "name": "thumbnail_u_r_l",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filesize": {
+ "name": "filesize",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "focal_x": {
+ "name": "focal_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "focal_y": {
+ "name": "focal_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_thumbnail_url": {
+ "name": "sizes_thumbnail_url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_thumbnail_width": {
+ "name": "sizes_thumbnail_width",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_thumbnail_height": {
+ "name": "sizes_thumbnail_height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_thumbnail_mime_type": {
+ "name": "sizes_thumbnail_mime_type",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_thumbnail_filesize": {
+ "name": "sizes_thumbnail_filesize",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_thumbnail_filename": {
+ "name": "sizes_thumbnail_filename",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_hero_url": {
+ "name": "sizes_hero_url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_hero_width": {
+ "name": "sizes_hero_width",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_hero_height": {
+ "name": "sizes_hero_height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_hero_mime_type": {
+ "name": "sizes_hero_mime_type",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_hero_filesize": {
+ "name": "sizes_hero_filesize",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sizes_hero_filename": {
+ "name": "sizes_hero_filename",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "media_updated_at_idx": {
+ "name": "media_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_created_at_idx": {
+ "name": "media_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_filename_idx": {
+ "name": "media_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_sizes_thumbnail_sizes_thumbnail_filename_idx": {
+ "name": "media_sizes_thumbnail_sizes_thumbnail_filename_idx",
+ "columns": [
+ {
+ "expression": "sizes_thumbnail_filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_sizes_hero_sizes_hero_filename_idx": {
+ "name": "media_sizes_hero_sizes_hero_filename_idx",
+ "columns": [
+ {
+ "expression": "sizes_hero_filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_kv": {
+ "name": "payload_kv",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "payload_kv_key_idx": {
+ "name": "payload_kv_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_locked_documents": {
+ "name": "payload_locked_documents",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "global_slug": {
+ "name": "global_slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_locked_documents_global_slug_idx": {
+ "name": "payload_locked_documents_global_slug_idx",
+ "columns": [
+ {
+ "expression": "global_slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_updated_at_idx": {
+ "name": "payload_locked_documents_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_created_at_idx": {
+ "name": "payload_locked_documents_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_locked_documents_rels": {
+ "name": "payload_locked_documents_rels",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "posts_id": {
+ "name": "posts_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "payload_locked_documents_rels_order_idx": {
+ "name": "payload_locked_documents_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_parent_idx": {
+ "name": "payload_locked_documents_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_path_idx": {
+ "name": "payload_locked_documents_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_users_id_idx": {
+ "name": "payload_locked_documents_rels_users_id_idx",
+ "columns": [
+ {
+ "expression": "users_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_posts_id_idx": {
+ "name": "payload_locked_documents_rels_posts_id_idx",
+ "columns": [
+ {
+ "expression": "posts_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_media_id_idx": {
+ "name": "payload_locked_documents_rels_media_id_idx",
+ "columns": [
+ {
+ "expression": "media_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "payload_locked_documents_rels_parent_fk": {
+ "name": "payload_locked_documents_rels_parent_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "payload_locked_documents",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_users_fk": {
+ "name": "payload_locked_documents_rels_users_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "users",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_posts_fk": {
+ "name": "payload_locked_documents_rels_posts_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "posts",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "posts_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_media_fk": {
+ "name": "payload_locked_documents_rels_media_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "media",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_preferences": {
+ "name": "payload_preferences",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_preferences_key_idx": {
+ "name": "payload_preferences_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_updated_at_idx": {
+ "name": "payload_preferences_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_created_at_idx": {
+ "name": "payload_preferences_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_preferences_rels": {
+ "name": "payload_preferences_rels",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "payload_preferences_rels_order_idx": {
+ "name": "payload_preferences_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_parent_idx": {
+ "name": "payload_preferences_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_path_idx": {
+ "name": "payload_preferences_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_users_id_idx": {
+ "name": "payload_preferences_rels_users_id_idx",
+ "columns": [
+ {
+ "expression": "users_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "payload_preferences_rels_parent_fk": {
+ "name": "payload_preferences_rels_parent_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "payload_preferences",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_preferences_rels_users_fk": {
+ "name": "payload_preferences_rels_users_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "users",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_migrations": {
+ "name": "payload_migrations",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "batch": {
+ "name": "batch",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_migrations_updated_at_idx": {
+ "name": "payload_migrations_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_migrations_created_at_idx": {
+ "name": "payload_migrations_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "payload.enum_posts_status": {
+ "name": "enum_posts_status",
+ "schema": "payload",
+ "values": [
+ "draft",
+ "published"
+ ]
+ },
+ "payload.enum__posts_v_version_status": {
+ "name": "enum__posts_v_version_status",
+ "schema": "payload",
+ "values": [
+ "draft",
+ "published"
+ ]
+ }
+ },
+ "schemas": {
+ "payload": "payload"
+ },
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "id": "d3290989-1d0a-421f-83f9-da08029563d4",
+ "prevId": "00000000-0000-0000-0000-000000000000"
+}
\ No newline at end of file
diff --git a/nextjs-app/migrations/20260902_172826_initial.ts b/nextjs-app/migrations/20260902_172826_initial.ts
new file mode 100644
index 0000000..aedc847
--- /dev/null
+++ b/nextjs-app/migrations/20260902_172826_initial.ts
@@ -0,0 +1,216 @@
+import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
+
+export async function up({ db, payload, req }: MigrateUpArgs): Promise {
+ /*
+ * Hand-added, and it must survive any regeneration of this file.
+ *
+ * `schemaName: 'payload'` tells Payload where to put its tables; it does not
+ * create the schema. Every statement below is qualified to "payload", so on
+ * a database that has never run this (staging and production both), the
+ * whole migration fails with `schema "payload" does not exist`. The schema
+ * only existed on the throwaway database used to generate this because it
+ * was created there by hand.
+ */
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS "payload";`)
+
+ await db.execute(sql`
+ CREATE TYPE "payload"."enum_posts_status" AS ENUM('draft', 'published');
+ CREATE TYPE "payload"."enum__posts_v_version_status" AS ENUM('draft', 'published');
+ CREATE TABLE "payload"."users_sessions" (
+ "_order" integer NOT NULL,
+ "_parent_id" integer NOT NULL,
+ "id" varchar PRIMARY KEY NOT NULL,
+ "created_at" timestamp(3) with time zone,
+ "expires_at" timestamp(3) with time zone NOT NULL
+ );
+
+ CREATE TABLE "payload"."users" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "display_name" varchar DEFAULT 'Tudor' NOT NULL,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "email" varchar NOT NULL,
+ "reset_password_token" varchar,
+ "reset_password_expiration" timestamp(3) with time zone,
+ "salt" varchar,
+ "hash" varchar,
+ "login_attempts" numeric DEFAULT 0,
+ "lock_until" timestamp(3) with time zone
+ );
+
+ CREATE TABLE "payload"."posts" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "title" varchar,
+ "slug" varchar,
+ "published_at" timestamp(3) with time zone,
+ "excerpt" varchar,
+ "hero_image_id" integer,
+ "content" jsonb,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "_status" "payload"."enum_posts_status" DEFAULT 'draft'
+ );
+
+ CREATE TABLE "payload"."_posts_v" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "parent_id" integer,
+ "version_title" varchar,
+ "version_slug" varchar,
+ "version_published_at" timestamp(3) with time zone,
+ "version_excerpt" varchar,
+ "version_hero_image_id" integer,
+ "version_content" jsonb,
+ "version_updated_at" timestamp(3) with time zone,
+ "version_created_at" timestamp(3) with time zone,
+ "version__status" "payload"."enum__posts_v_version_status" DEFAULT 'draft',
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "latest" boolean
+ );
+
+ CREATE TABLE "payload"."media" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "alt" varchar NOT NULL,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "url" varchar,
+ "thumbnail_u_r_l" varchar,
+ "filename" varchar,
+ "mime_type" varchar,
+ "filesize" numeric,
+ "width" numeric,
+ "height" numeric,
+ "focal_x" numeric,
+ "focal_y" numeric,
+ "sizes_thumbnail_url" varchar,
+ "sizes_thumbnail_width" numeric,
+ "sizes_thumbnail_height" numeric,
+ "sizes_thumbnail_mime_type" varchar,
+ "sizes_thumbnail_filesize" numeric,
+ "sizes_thumbnail_filename" varchar,
+ "sizes_hero_url" varchar,
+ "sizes_hero_width" numeric,
+ "sizes_hero_height" numeric,
+ "sizes_hero_mime_type" varchar,
+ "sizes_hero_filesize" numeric,
+ "sizes_hero_filename" varchar
+ );
+
+ CREATE TABLE "payload"."payload_kv" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "key" varchar NOT NULL,
+ "data" jsonb NOT NULL
+ );
+
+ CREATE TABLE "payload"."payload_locked_documents" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "global_slug" varchar,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "payload"."payload_locked_documents_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "users_id" integer,
+ "posts_id" integer,
+ "media_id" integer
+ );
+
+ CREATE TABLE "payload"."payload_preferences" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "key" varchar,
+ "value" jsonb,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "payload"."payload_preferences_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "users_id" integer
+ );
+
+ CREATE TABLE "payload"."payload_migrations" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar,
+ "batch" numeric,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ ALTER TABLE "payload"."users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "payload"."users"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."posts" ADD CONSTRAINT "posts_hero_image_id_media_id_fk" FOREIGN KEY ("hero_image_id") REFERENCES "payload"."media"("id") ON DELETE set null ON UPDATE no action;
+ ALTER TABLE "payload"."_posts_v" ADD CONSTRAINT "_posts_v_parent_id_posts_id_fk" FOREIGN KEY ("parent_id") REFERENCES "payload"."posts"("id") ON DELETE set null ON UPDATE no action;
+ ALTER TABLE "payload"."_posts_v" ADD CONSTRAINT "_posts_v_version_hero_image_id_media_id_fk" FOREIGN KEY ("version_hero_image_id") REFERENCES "payload"."media"("id") ON DELETE set null ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "payload"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "payload"."users"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "payload"."posts"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "payload"."media"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "payload"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "payload"."users"("id") ON DELETE cascade ON UPDATE no action;
+ CREATE INDEX "users_sessions_order_idx" ON "payload"."users_sessions" USING btree ("_order");
+ CREATE INDEX "users_sessions_parent_id_idx" ON "payload"."users_sessions" USING btree ("_parent_id");
+ CREATE INDEX "users_updated_at_idx" ON "payload"."users" USING btree ("updated_at");
+ CREATE INDEX "users_created_at_idx" ON "payload"."users" USING btree ("created_at");
+ CREATE UNIQUE INDEX "users_email_idx" ON "payload"."users" USING btree ("email");
+ CREATE UNIQUE INDEX "posts_slug_idx" ON "payload"."posts" USING btree ("slug");
+ CREATE INDEX "posts_hero_image_idx" ON "payload"."posts" USING btree ("hero_image_id");
+ CREATE INDEX "posts_updated_at_idx" ON "payload"."posts" USING btree ("updated_at");
+ CREATE INDEX "posts_created_at_idx" ON "payload"."posts" USING btree ("created_at");
+ CREATE INDEX "posts__status_idx" ON "payload"."posts" USING btree ("_status");
+ CREATE INDEX "_posts_v_parent_idx" ON "payload"."_posts_v" USING btree ("parent_id");
+ CREATE INDEX "_posts_v_version_version_slug_idx" ON "payload"."_posts_v" USING btree ("version_slug");
+ CREATE INDEX "_posts_v_version_version_hero_image_idx" ON "payload"."_posts_v" USING btree ("version_hero_image_id");
+ CREATE INDEX "_posts_v_version_version_updated_at_idx" ON "payload"."_posts_v" USING btree ("version_updated_at");
+ CREATE INDEX "_posts_v_version_version_created_at_idx" ON "payload"."_posts_v" USING btree ("version_created_at");
+ CREATE INDEX "_posts_v_version_version__status_idx" ON "payload"."_posts_v" USING btree ("version__status");
+ CREATE INDEX "_posts_v_created_at_idx" ON "payload"."_posts_v" USING btree ("created_at");
+ CREATE INDEX "_posts_v_updated_at_idx" ON "payload"."_posts_v" USING btree ("updated_at");
+ CREATE INDEX "_posts_v_latest_idx" ON "payload"."_posts_v" USING btree ("latest");
+ CREATE INDEX "media_updated_at_idx" ON "payload"."media" USING btree ("updated_at");
+ CREATE INDEX "media_created_at_idx" ON "payload"."media" USING btree ("created_at");
+ CREATE UNIQUE INDEX "media_filename_idx" ON "payload"."media" USING btree ("filename");
+ CREATE INDEX "media_sizes_thumbnail_sizes_thumbnail_filename_idx" ON "payload"."media" USING btree ("sizes_thumbnail_filename");
+ CREATE INDEX "media_sizes_hero_sizes_hero_filename_idx" ON "payload"."media" USING btree ("sizes_hero_filename");
+ CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload"."payload_kv" USING btree ("key");
+ CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload"."payload_locked_documents" USING btree ("global_slug");
+ CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload"."payload_locked_documents" USING btree ("updated_at");
+ CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload"."payload_locked_documents" USING btree ("created_at");
+ CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload"."payload_locked_documents_rels" USING btree ("order");
+ CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload"."payload_locked_documents_rels" USING btree ("parent_id");
+ CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload"."payload_locked_documents_rels" USING btree ("path");
+ CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload"."payload_locked_documents_rels" USING btree ("users_id");
+ CREATE INDEX "payload_locked_documents_rels_posts_id_idx" ON "payload"."payload_locked_documents_rels" USING btree ("posts_id");
+ CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload"."payload_locked_documents_rels" USING btree ("media_id");
+ CREATE INDEX "payload_preferences_key_idx" ON "payload"."payload_preferences" USING btree ("key");
+ CREATE INDEX "payload_preferences_updated_at_idx" ON "payload"."payload_preferences" USING btree ("updated_at");
+ CREATE INDEX "payload_preferences_created_at_idx" ON "payload"."payload_preferences" USING btree ("created_at");
+ CREATE INDEX "payload_preferences_rels_order_idx" ON "payload"."payload_preferences_rels" USING btree ("order");
+ CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload"."payload_preferences_rels" USING btree ("parent_id");
+ CREATE INDEX "payload_preferences_rels_path_idx" ON "payload"."payload_preferences_rels" USING btree ("path");
+ CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload"."payload_preferences_rels" USING btree ("users_id");
+ CREATE INDEX "payload_migrations_updated_at_idx" ON "payload"."payload_migrations" USING btree ("updated_at");
+ CREATE INDEX "payload_migrations_created_at_idx" ON "payload"."payload_migrations" USING btree ("created_at");`)
+}
+
+export async function down({ db, payload, req }: MigrateDownArgs): Promise {
+ await db.execute(sql`
+ DROP TABLE "payload"."users_sessions" CASCADE;
+ DROP TABLE "payload"."users" CASCADE;
+ DROP TABLE "payload"."posts" CASCADE;
+ DROP TABLE "payload"."_posts_v" CASCADE;
+ DROP TABLE "payload"."media" CASCADE;
+ DROP TABLE "payload"."payload_kv" CASCADE;
+ DROP TABLE "payload"."payload_locked_documents" CASCADE;
+ DROP TABLE "payload"."payload_locked_documents_rels" CASCADE;
+ DROP TABLE "payload"."payload_preferences" CASCADE;
+ DROP TABLE "payload"."payload_preferences_rels" CASCADE;
+ DROP TABLE "payload"."payload_migrations" CASCADE;
+ DROP TYPE "payload"."enum_posts_status";
+ DROP TYPE "payload"."enum__posts_v_version_status";`)
+}
diff --git a/nextjs-app/migrations/index.ts b/nextjs-app/migrations/index.ts
new file mode 100644
index 0000000..ba271c4
--- /dev/null
+++ b/nextjs-app/migrations/index.ts
@@ -0,0 +1,9 @@
+import * as migration_20260902_172826_initial from './20260902_172826_initial';
+
+export const migrations = [
+ {
+ up: migration_20260902_172826_initial.up,
+ down: migration_20260902_172826_initial.down,
+ name: '20260902_172826_initial'
+ },
+];
diff --git a/nextjs-app/payload-types.ts b/nextjs-app/payload-types.ts
new file mode 100644
index 0000000..41b8b7c
--- /dev/null
+++ b/nextjs-app/payload-types.ts
@@ -0,0 +1,443 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * This file was automatically generated by Payload.
+ * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
+ * and re-run `payload generate:types` to regenerate this file.
+ */
+
+/**
+ * Supported timezones in IANA format.
+ *
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "supportedTimezones".
+ */
+export type SupportedTimezones =
+ | 'Pacific/Midway'
+ | 'Pacific/Niue'
+ | 'Pacific/Honolulu'
+ | 'Pacific/Rarotonga'
+ | 'America/Anchorage'
+ | 'Pacific/Gambier'
+ | 'America/Los_Angeles'
+ | 'America/Tijuana'
+ | 'America/Denver'
+ | 'America/Phoenix'
+ | 'America/Chicago'
+ | 'America/Guatemala'
+ | 'America/New_York'
+ | 'America/Bogota'
+ | 'America/Caracas'
+ | 'America/Santiago'
+ | 'America/Buenos_Aires'
+ | 'America/Sao_Paulo'
+ | 'Atlantic/South_Georgia'
+ | 'Atlantic/Azores'
+ | 'Atlantic/Cape_Verde'
+ | 'Europe/London'
+ | 'Europe/Berlin'
+ | 'Africa/Lagos'
+ | 'Europe/Athens'
+ | 'Africa/Cairo'
+ | 'Europe/Moscow'
+ | 'Asia/Riyadh'
+ | 'Asia/Dubai'
+ | 'Asia/Baku'
+ | 'Asia/Karachi'
+ | 'Asia/Tashkent'
+ | 'Asia/Calcutta'
+ | 'Asia/Dhaka'
+ | 'Asia/Almaty'
+ | 'Asia/Jakarta'
+ | 'Asia/Bangkok'
+ | 'Asia/Shanghai'
+ | 'Asia/Singapore'
+ | 'Asia/Tokyo'
+ | 'Asia/Seoul'
+ | 'Australia/Brisbane'
+ | 'Australia/Sydney'
+ | 'Pacific/Guam'
+ | 'Pacific/Noumea'
+ | 'Pacific/Auckland'
+ | 'Pacific/Fiji';
+
+export interface Config {
+ auth: {
+ users: UserAuthOperations;
+ };
+ blocks: {};
+ collections: {
+ users: User;
+ posts: Post;
+ media: Media;
+ 'payload-kv': PayloadKv;
+ 'payload-locked-documents': PayloadLockedDocument;
+ 'payload-preferences': PayloadPreference;
+ 'payload-migrations': PayloadMigration;
+ };
+ collectionsJoins: {};
+ collectionsSelect: {
+ users: UsersSelect | UsersSelect;
+ posts: PostsSelect | PostsSelect;
+ media: MediaSelect | MediaSelect;
+ 'payload-kv': PayloadKvSelect | PayloadKvSelect;
+ 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect;
+ 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect;
+ 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect;
+ };
+ db: {
+ defaultIDType: number;
+ };
+ fallbackLocale: null;
+ globals: {};
+ globalsSelect: {};
+ locale: null;
+ widgets: {
+ collections: CollectionsWidget;
+ };
+ user: User;
+ jobs: {
+ tasks: unknown;
+ workflows: unknown;
+ };
+}
+export interface UserAuthOperations {
+ forgotPassword: {
+ email: string;
+ password: string;
+ };
+ login: {
+ email: string;
+ password: string;
+ };
+ registerFirstUser: {
+ email: string;
+ password: string;
+ };
+ unlock: {
+ email: string;
+ password: string;
+ };
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "users".
+ */
+export interface User {
+ id: number;
+ displayName: string;
+ updatedAt: string;
+ createdAt: string;
+ email: string;
+ resetPasswordToken?: string | null;
+ resetPasswordExpiration?: string | null;
+ salt?: string | null;
+ hash?: string | null;
+ loginAttempts?: number | null;
+ lockUntil?: string | null;
+ sessions?:
+ | {
+ id: string;
+ createdAt?: string | null;
+ expiresAt: string;
+ }[]
+ | null;
+ password?: string | null;
+ collection: 'users';
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "posts".
+ */
+export interface Post {
+ id: number;
+ title: string;
+ /**
+ * The URL segment. Never change it after publishing.
+ */
+ slug: string;
+ publishedAt: string;
+ /**
+ * Shown on the index and used as the meta description.
+ */
+ excerpt: string;
+ heroImage?: (number | null) | Media;
+ content: {
+ root: {
+ type: string;
+ children: {
+ type: any;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ };
+ updatedAt: string;
+ createdAt: string;
+ _status?: ('draft' | 'published') | null;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "media".
+ */
+export interface Media {
+ id: number;
+ /**
+ * Describe the image for screen readers.
+ */
+ alt: string;
+ updatedAt: string;
+ createdAt: string;
+ url?: string | null;
+ thumbnailURL?: string | null;
+ filename?: string | null;
+ mimeType?: string | null;
+ filesize?: number | null;
+ width?: number | null;
+ height?: number | null;
+ focalX?: number | null;
+ focalY?: number | null;
+ sizes?: {
+ thumbnail?: {
+ url?: string | null;
+ width?: number | null;
+ height?: number | null;
+ mimeType?: string | null;
+ filesize?: number | null;
+ filename?: string | null;
+ };
+ hero?: {
+ url?: string | null;
+ width?: number | null;
+ height?: number | null;
+ mimeType?: string | null;
+ filesize?: number | null;
+ filename?: string | null;
+ };
+ };
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-kv".
+ */
+export interface PayloadKv {
+ id: number;
+ key: string;
+ data:
+ | {
+ [k: string]: unknown;
+ }
+ | unknown[]
+ | string
+ | number
+ | boolean
+ | null;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-locked-documents".
+ */
+export interface PayloadLockedDocument {
+ id: number;
+ document?:
+ | ({
+ relationTo: 'users';
+ value: number | User;
+ } | null)
+ | ({
+ relationTo: 'posts';
+ value: number | Post;
+ } | null)
+ | ({
+ relationTo: 'media';
+ value: number | Media;
+ } | null);
+ globalSlug?: string | null;
+ user: {
+ relationTo: 'users';
+ value: number | User;
+ };
+ updatedAt: string;
+ createdAt: string;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-preferences".
+ */
+export interface PayloadPreference {
+ id: number;
+ user: {
+ relationTo: 'users';
+ value: number | User;
+ };
+ key?: string | null;
+ value?:
+ | {
+ [k: string]: unknown;
+ }
+ | unknown[]
+ | string
+ | number
+ | boolean
+ | null;
+ updatedAt: string;
+ createdAt: string;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-migrations".
+ */
+export interface PayloadMigration {
+ id: number;
+ name?: string | null;
+ batch?: number | null;
+ updatedAt: string;
+ createdAt: string;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "users_select".
+ */
+export interface UsersSelect {
+ displayName?: T;
+ updatedAt?: T;
+ createdAt?: T;
+ email?: T;
+ resetPasswordToken?: T;
+ resetPasswordExpiration?: T;
+ salt?: T;
+ hash?: T;
+ loginAttempts?: T;
+ lockUntil?: T;
+ sessions?:
+ | T
+ | {
+ id?: T;
+ createdAt?: T;
+ expiresAt?: T;
+ };
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "posts_select".
+ */
+export interface PostsSelect {
+ title?: T;
+ slug?: T;
+ publishedAt?: T;
+ excerpt?: T;
+ heroImage?: T;
+ content?: T;
+ updatedAt?: T;
+ createdAt?: T;
+ _status?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "media_select".
+ */
+export interface MediaSelect {
+ alt?: T;
+ updatedAt?: T;
+ createdAt?: T;
+ url?: T;
+ thumbnailURL?: T;
+ filename?: T;
+ mimeType?: T;
+ filesize?: T;
+ width?: T;
+ height?: T;
+ focalX?: T;
+ focalY?: T;
+ sizes?:
+ | T
+ | {
+ thumbnail?:
+ | T
+ | {
+ url?: T;
+ width?: T;
+ height?: T;
+ mimeType?: T;
+ filesize?: T;
+ filename?: T;
+ };
+ hero?:
+ | T
+ | {
+ url?: T;
+ width?: T;
+ height?: T;
+ mimeType?: T;
+ filesize?: T;
+ filename?: T;
+ };
+ };
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-kv_select".
+ */
+export interface PayloadKvSelect {
+ key?: T;
+ data?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-locked-documents_select".
+ */
+export interface PayloadLockedDocumentsSelect {
+ document?: T;
+ globalSlug?: T;
+ user?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-preferences_select".
+ */
+export interface PayloadPreferencesSelect {
+ user?: T;
+ key?: T;
+ value?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "payload-migrations_select".
+ */
+export interface PayloadMigrationsSelect {
+ name?: T;
+ batch?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "collections_widget".
+ */
+export interface CollectionsWidget {
+ data?: {
+ [k: string]: unknown;
+ };
+ width: 'full';
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "auth".
+ */
+export interface Auth {
+ [k: string]: unknown;
+}
+
+
+declare module 'payload' {
+ export interface GeneratedTypes extends Config {}
+}
\ No newline at end of file
diff --git a/nextjs-app/payload.config.ts b/nextjs-app/payload.config.ts
index 6be8850..5d53597 100644
--- a/nextjs-app/payload.config.ts
+++ b/nextjs-app/payload.config.ts
@@ -8,6 +8,7 @@ import { Users } from '@/collections/Users';
import { Posts } from '@/collections/Posts';
import { Media } from '@/collections/Media';
import { PAYLOAD_API_ROUTE, PAYLOAD_ADMIN_ROUTE } from '@/lib/payloadRoutes';
+import { migrations } from '@/migrations';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
@@ -25,8 +26,15 @@ export default buildConfig({
pool: { connectionString: process.env.DATABASE_URL },
// Its own schema, so no pipeline operation on `public` can reach blog
// content. scripts/migrate_csv_to_db.py --drop lives in that blast radius,
- // as does Airflow's metadata.
+ // as does Airflow's metadata. The schema itself is created by the initial
+ // migration: schemaName says where tables go, it does not create anything.
+ //
+ // prodMigrations runs pending migrations during server init. Without it a
+ // production container connects to an empty schema and fails its first
+ // query with 42P01 — the adapter cannot self-create tables, because
+ // db-postgres/connect.js gates push on NODE_ENV !== 'production'.
schemaName: 'payload',
+ prodMigrations: migrations,
}),
sharp,
});