import Link from 'next/link';

import { DocsShell } from '../../../components/shell';

export const metadata = { title: 'migration · hasura → briven' };

export default function HasuraMigrationPage() {
  return (
    <DocsShell>
      <p className="font-mono text-xs text-[var(--color-text-muted)]">
        <Link href="/migration" className="hover:text-[var(--color-text)]">
          ← migration
        </Link>
      </p>
      <h1 className="mt-2 font-mono text-2xl tracking-tight">hasura → briven</h1>
      <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
        hasura is &quot;graphql in front of your postgres&quot;. briven is &quot;reactive
        functions in front of your postgres&quot;. the postgres half ports cleanly; the
        graphql + permissions half becomes function code. plan a 3-5 day window for a
        mid-size project (50-150 tables, a dozen actions, a handful of remote schemas).
      </p>

      <div className="mt-6 rounded-md border border-[var(--color-text-error)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
        <strong>read this first:</strong> the trap with hasura migrations is that the
        schema port looks trivial (it&apos;s the same postgres!) and then the permissions
        port doubles your timeline. <em>permissions are not optional</em> — every public
        graphql query you exposed had a permission rule, and skipping any of them in the
        port turns a private query into an open endpoint. step 4 of the playbook is where
        90% of the work lives.
      </div>

      <Section title="what carries over for free">
        <ul className="list-disc pl-5">
          <li>
            <strong>tables, columns, indexes, foreign keys, constraints</strong> —{' '}
            <code>pg_dump --schema-only</code> from the hasura postgres and load it into a
            scratch postgres; that file is 95% of <code>briven/schema.ts</code>. translate
            with the cli&apos;s <code>briven import --schema-sql ./schema.sql</code>
            generator (best-effort; eyeball before committing).
          </li>
          <li>
            <strong>postgres functions + triggers + extensions</strong> —{' '}
            <code>pg_dump</code> includes them; briven&apos;s data plane runs the same
            pg17+pgvector image. enable extensions explicitly via the schema dsl so the
            migration runner knows to load them on a fresh project.
          </li>
          <li>
            <strong>row data</strong> — <code>pg_dump --data-only</code> →{' '}
            <code>pg_restore</code> through <code>briven db shell-token</code>. row-count
            both sides; numbers must match.
          </li>
        </ul>
      </Section>

      <Section title="what becomes function code">
        <p>
          everything graphql-shaped becomes a briven query/mutation. hasura&apos;s metadata
          maps as follows:
        </p>
        <ul className="list-disc pl-5">
          <li>
            <strong>tracked tables / autogenerated queries</strong> — write one{' '}
            <code>query()</code> per &quot;table-as-graphql-root&quot; surface your clients
            actually use. don&apos;t port every autogenerated field — most of them aren&apos;t
            called. grep your frontend for the queries you actually send, port only those.
          </li>
          <li>
            <strong>relationships</strong> — hasura denormalises related rows inline (
            <code>{`users(... { posts { ... } })`}</code>). in briven this is one or two joins
            inside the function, or a sibling <code>useQuery</code> on the client. for nested
            shapes, prefer the join in the function — fewer roundtrips, easier auth.
          </li>
          <li>
            <strong>actions (HTTP webhooks)</strong> — direct port to <code>mutation()</code>{' '}
            functions. the action&apos;s url + handler becomes the body of the function.
          </li>
          <li>
            <strong>event triggers</strong> — port to either (a) a briven function called from
            an outbox table that postgres triggers write into, or (b) a postgres trigger that
            calls <code>pg_notify</code> and a briven listener function picks up. (a) is
            simpler; (b) is lower latency.
          </li>
          <li>
            <strong>scheduled triggers (cron)</strong> — briven has a cron primitive coming
            in v1; until then, ship a github actions / external scheduler that hits a briven
            function on a schedule.
          </li>
          <li>
            <strong>remote schemas</strong> — these were &quot;merge another graphql api into
            ours.&quot; in briven, write a function that fetches from the remote api in
            its handler and returns the data. you lose graphql stitching; you gain typed
            request/response shapes.
          </li>
        </ul>
      </Section>

      <Section title="permissions port">
        <p>
          hasura permissions live in metadata: per-table, per-role, with select/insert/update/
          delete columns + a row filter expression. <em>every one of them</em> needs an
          equivalent in your function code. there is no shortcut.
        </p>
        <Snippet>{`// hasura: a select permission on "notes" for role "user"
// filter: { user_id: { _eq: "X-Hasura-User-Id" } }
// columns: [id, body, created_at]

// briven equivalent — inside briven/functions/notes.ts
import { query } from '@briven/cli/server';

export const list = query({
  args: { /* none — the user is identified by the session */ },
  handler: async (ctx) => {
    const userId = ctx.user?.id;
    if (!userId) throw new Error('unauthorized');
    const rows = await ctx.db('notes')
      .select(['id', 'body', 'created_at'])  // mirror the column allowlist
      .where({ user_id: userId });           // mirror the filter
    return rows;
  },
});`}</Snippet>
        <p>
          the pattern is mechanical but tedious. inventory every (role, table, action) triple
          from your hasura metadata before writing any function code — that list is the work.
        </p>
      </Section>

      <Section title="auth port">
        <p>
          hasura&apos;s auth is &quot;decode a jwt, extract X-Hasura-User-Id, apply
          permissions.&quot; briven&apos;s auth is Better Auth — sessions over cookies,
          token-based for headless clients. two cutover options:
        </p>
        <ul className="list-disc pl-5">
          <li>
            <strong>preserve user ids</strong> — easiest. your hasura users table has stable
            ids; export them as-is into briven&apos;s <code>users</code> table via the data
            copy. users sign in fresh (forced re-auth) but keep their data and links.
          </li>
          <li>
            <strong>preserve jwts</strong> — if you can&apos;t force a re-auth (consumer app
            with many active sessions), keep your existing auth issuer and validate its jwts
            in a briven middleware. file a support ticket — this path is supported but not
            self-service yet.
          </li>
        </ul>
      </Section>

      <Section title="subscriptions port">
        <p>
          hasura&apos;s <code>subscription</code> → briven&apos;s reactive{' '}
          <code>useQuery(&quot;getThing&quot;, args)</code>. the shapes look identical from
          the client&apos;s side; the wire protocol is briven&apos;s. one difference worth
          calling out:
        </p>
        <ul className="list-disc pl-5">
          <li>
            hasura sends incremental updates over a long-lived websocket; briven re-runs the
            function on every relevant LISTEN/NOTIFY and pushes the full new result. for very
            large result sets this is more bytes per update — paginate the function or split
            into smaller subscriptions if it matters.
          </li>
          <li>
            hasura aggregations (<code>_aggregate</code>) over a live query are heavy; the
            same logic in a briven function lets you pre-compute or cache. mostly an upgrade.
          </li>
        </ul>
      </Section>

      <Section title="cutover checklist">
        <ul className="list-disc pl-5">
          <li>schema + indexes + extensions match (pg_dump diff)</li>
          <li>every (role, table, action) permission has a function equivalent</li>
          <li>every active client query in your frontend has a briven function</li>
          <li>actions ported + tested with real upstream</li>
          <li>event triggers wired</li>
          <li>auth strategy decided (preserve ids vs preserve jwts)</li>
          <li>48-hour parallel-run window planned + observed</li>
          <li>hasura console set to read-only after cutover for 7 days</li>
        </ul>
      </Section>
    </DocsShell>
  );
}

function Section({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <section className="mt-10">
      <h2 className="font-mono text-lg">{title}</h2>
      <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]">
        {children}
      </div>
    </section>
  );
}

function Snippet({ children }: { children: string }) {
  return (
    <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs">
      <code>{children}</code>
    </pre>
  );
}
