{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/supabase-basics-nodejs/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-06-03 23:23:30 +0200"}},"id":"7de379dd-ecd6-54a9-b00e-2e6be9df50f6","excerpt":"Supabase  is an open-source backend platform built around managed PostgreSQL. You get a database, auto-generated REST APIs (via  PostgREST…","frontmatter":{"title":"Supabase basics with Node.js","date":"2026-06-03 12:00:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["supabase","postgres","api","node"],"cover":null},"fields":{"slug":"/notes/supabase-basics-nodejs/","readingTime":{"text":"6 min read"}},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Supabase basics with Node.js\",\n  \"date\": \"2026-06-03 12:00:00 UTC\",\n  \"tags\": [\"supabase\", \"postgres\", \"api\", \"node\"],\n  \"canonical_url\": \"https://sevic.dev/notes/supabase-basics-nodejs/\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://supabase.com/\"\n  }), \"Supabase\"), \" is an open-source backend platform built around managed PostgreSQL. You get a database, auto-generated REST APIs (via \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://docs.postgrest.org/en/v14/\"\n  }), \"PostgREST\"), \"), Auth, file Storage, Realtime subscriptions, and Edge Functions - with a dashboard and SQL editor on top.\"), mdx(\"p\", null, \"Compared to running Postgres yourself, Supabase adds hosted infra, API layers, and product features without you wiring them up. Compared to an ORM-only stack, you often talk to Postgres through the Supabase client or SQL, with RLS (row level security) enforcing access at the database layer.\"), mdx(\"h3\", {\n    \"id\": \"prerequisites\"\n  }, \"Prerequisites\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Supabase account (free tier is enough for learning)\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Node.js version 26\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"@supabase/supabase-js\"), \" installed (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"npm i @supabase/supabase-js\"), \")\")), mdx(\"h3\", {\n    \"id\": \"create-a-project-and-database\"\n  }, \"Create a project and database\"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"In the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://supabase.com/dashboard\"\n  }), \"Supabase dashboard\"), \", choose \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"New project\"), \", pick a region, and set the database password.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Wait until the project is ready.\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Open \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"SQL Editor\"), \" and run the schema below.\")), mdx(\"p\", null, \"The \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Table Editor\"), \" is fine for quick experiments; \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"SQL Editor\"), \" keeps schema reproducible in git and reviews.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-sql\"\n  }), \"create table if not exists public.todos (\\n  id bigint generated always as identity primary key,\\n  title text not null,\\n  done boolean not null default false,\\n  created_at timestamptz not null default now()\\n);\\n\\ncreate or replace function public.list_open_todos()\\nreturns setof public.todos\\nlanguage sql\\nsecurity definer\\nset search_path = public\\nas $$\\n  select * from public.todos where done = false order by id;\\n$$;\\n\\ncreate or replace function public.mark_todo_done(todo_id bigint)\\nreturns setof public.todos\\nlanguage sql\\nsecurity definer\\nset search_path = public\\nas $$\\n  update public.todos set done = true where id = todo_id returning *;\\n$$;\\n\\ngrant execute on function public.list_open_todos() to service_role;\\ngrant execute on function public.mark_todo_done(bigint) to service_role;\\n\")), mdx(\"p\", null, \"This schema skips RLS setup because this post uses the \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"secret\"), \" key from Node.js, which bypasses RLS. For browser or mobile clients, use the \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"publishable\"), \" key instead - it obeys \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://supabase.com/docs/guides/database/postgres/row-level-security\"\n  }), \"Row Level Security\"), \" when you enable it on tables.\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"list_open_todos\"), \" is a \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Postgres function\"), \" exposed as an RPC endpoint.\"), mdx(\"p\", null, \"Postgres functions default to \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"security invoker\")), \": they run with the caller's permissions, so RLS applies as that role. \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"security definer\")), \" runs the function as its \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"owner\"), \" (often a privileged database role), not as the caller.\"), mdx(\"h3\", {\n    \"id\": \"api-keys-and-connection\"\n  }, \"API keys and connection\"), mdx(\"p\", null, \"On the project \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Overview\"), \" tab you will find:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Project URL\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"https://<ref>.supabase.co\"), \") \\u2014 for \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"SUPABASE_URL\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Publishable key\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"sb_publishable_...\"), \", legacy \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"anon\"), \") \\u2014 for browsers and mobile apps where RLS should limit access; this post does not use it\")), mdx(\"p\", null, \"For Node.js backends, use a \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"secret\"), \" key from \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Project Settings\"), \" \\u2192 \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"API Keys\"), \":\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Secret key(s)\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"sb_secret_...\"), \") - full data access, bypasses RLS; trusted servers only, never in client bundles or public repos. Replaces the legacy \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"service_role\"), \" key\"), \".\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Reveal or create a secret key in the dashboard (legacy projects: \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Legacy anon, service_role API keys\"), \" tab \\u2192 \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"service_role\"), \").\")), mdx(\"p\", null, \"The Supabase JS client talks to \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"PostgREST\"), \" and can query tables (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"from\"), \") and call registered functions (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"rpc\"), \") only \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"after\"), \" they exist. It does not run \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"DDL\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"create table\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"create function\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"grant\"), \", and similar), so apply the schema in the \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"SQL Editor\"), \" (or via \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://supabase.com/docs/guides/cli\"\n  }), \"Supabase CLI\"), \" migrations in larger projects).\"), mdx(\"p\", null, \"Store values in environment variables:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"SUPABASE_URL=https://<ref>.supabase.co\\nSUPABASE_SECRET_KEY=your-secret-key\\n\")), mdx(\"h3\", {\n    \"id\": \"client-setup\"\n  }, \"Client setup\"), mdx(\"p\", null, \"Create a client with the project URL and secret key. Read values from the environment in real apps; never commit the secret key.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { createClient } from '@supabase/supabase-js';\\n\\nconst supabase = createClient(\\n  process.env.SUPABASE_URL,\\n  process.env.SUPABASE_SECRET_KEY\\n);\\n\")), mdx(\"p\", null, \"Every table you create in the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"public\"), \" schema is available as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"supabase.from('<table>')\"), \". Custom SQL functions are called with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"supabase.rpc('<function_name>')\"), \".\"), mdx(\"h3\", {\n    \"id\": \"insert-data\"\n  }, \"Insert data\"), mdx(\"p\", null, \"Insert one or more rows and return the created records with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".select()\"), \".\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const { data, error } = await supabase\\n  .from('todos')\\n  .insert([\\n    { title: 'Learn Supabase client', done: false },\\n    { title: 'Run RPC example', done: false },\\n    { title: 'Ship demo', done: true },\\n  ])\\n  .select();\\n\\nif (error) {\\n  throw new Error(error.message);\\n}\\n\\nconsole.log(data);\\n\")), mdx(\"h3\", {\n    \"id\": \"read-update-and-delete\"\n  }, \"Read, update, and delete\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Select\"), \" with filters and ordering:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const { data, error } = await supabase\\n  .from('todos')\\n  .select('*')\\n  .eq('done', false)\\n  .order('id');\\n\")), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Update\"), \" matching rows:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const { data, error } = await supabase\\n  .from('todos')\\n  .update({ done: true })\\n  .eq('id', 1)\\n  .select();\\n\")), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Delete\"), \" matching rows:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const { error } = await supabase\\n  .from('todos')\\n  .delete()\\n  .eq('id', 1);\\n\")), mdx(\"p\", null, \"Chain \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".eq()\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".in()\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".limit()\"), \", and other \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://supabase.com/docs/reference/javascript/filtering\"\n  }), \"filter helpers\"), \" the same way across operations.\"), mdx(\"h3\", {\n    \"id\": \"rpc\"\n  }, \"RPC\"), mdx(\"p\", null, \"Call a database function by name. Our \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"list_open_todos()\"), \" returns open todos without repeating filter logic in the app.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const { data, error } = await supabase.rpc('list_open_todos');\\n\\nif (error) {\\n  throw new Error(error.message);\\n}\\n\\nconsole.log(data);\\n\")), mdx(\"p\", null, \"Functions with parameters map to a second argument:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"await supabase.rpc('mark_todo_done', { todo_id: 42 });\\n\")), mdx(\"p\", null, \"Define parameters in SQL (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"todo_id bigint\"), \") and grant \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"execute\"), \" to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"service_role\"), \" when calling RPC from a secret-key backend.\"), mdx(\"h3\", {\n    \"id\": \"what-else-matters\"\n  }, \"What else matters\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Row Level Security (RLS)\"), \" - When enabled on a table, Postgres denies access until policies allow it. The \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"publishable\"), \" key is subject to RLS; the \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"secret\"), \" key is not. Add policies per role (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"anon\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"authenticated\"), \") and operation when you turn RLS on.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Auth\"), \" - Supabase Auth stores users in \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"auth.users\"), \". After sign-in, the client JWT includes the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"authenticated\"), \" role so policies can use \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"auth.uid()\"), \" for per-user rows.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Migrations\"), \" - Avoid changing production only via the dashboard. Track SQL in versioned migration files and apply with the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://supabase.com/docs/guides/cli\"\n  }), \"Supabase CLI\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"supabase db push\"), \", linked projects) so environments stay aligned.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Generated types\"), \" - Run \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"supabase gen types typescript --project-id <ref>\"), \" to emit TypeScript types from your schema for a type-safe client.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Storage\"), \" - S3-compatible buckets for files (images, PDFs) with bucket policies analogous to RLS.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Realtime\"), \" - Subscribe to \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"insert\"), \"/\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"update\"), \"/\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"delete\"), \" on tables or channels for live UI updates.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Edge Functions\"), \" - Deno functions for webhooks, light API logic, or tasks that should not live in the database.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Local dev\"), \" - \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"supabase init\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"supabase start\"), \" spin up local Postgres and services; useful for offline work, while cloud projects are fastest for a first tutorial.\")), mdx(\"h3\", {\n    \"id\": \"demo\"\n  }, \"Demo\"), mdx(\"p\", null, \"Runnable scripts for this post live in the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"supabase-basics-demo\"), \" folder in the private demos repository. Get access via \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/demos\"\n  }), \"code demos\"), \".\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"7de379dd-ecd6-54a9-b00e-2e6be9df50f6"}},"staticQueryHashes":["1961101537","2542493696"]}