diff --git a/examples/user_managment/phoenix_live_view_user_management/.env.dev b/examples/user_managment/phoenix_live_view_user_management/.env.dev
new file mode 100644
index 0000000..4ce3c0b
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/.env.dev
@@ -0,0 +1,8 @@
+export DATABASE_USER=postgres
+export DATABASE_PASS=postgres
+export DATABASE_HOST=127.0.0.1
+export DATABASE_NAME=postgres
+export DATABASE_PORT=54322
+
+export SUPABASE_URL=http://127.0.0.1:54321
+export SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
diff --git a/examples/user_managment/phoenix_live_view_user_management/.formatter.exs b/examples/user_managment/phoenix_live_view_user_management/.formatter.exs
new file mode 100644
index 0000000..ef8840c
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/.formatter.exs
@@ -0,0 +1,6 @@
+[
+ import_deps: [:ecto, :ecto_sql, :phoenix],
+ subdirectories: ["priv/*/migrations"],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
+]
diff --git a/examples/user_managment/phoenix_live_view_user_management/.gitignore b/examples/user_managment/phoenix_live_view_user_management/.gitignore
new file mode 100644
index 0000000..fcf7076
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/.gitignore
@@ -0,0 +1,37 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Temporary files, for example, from tests.
+/tmp/
+
+# Ignore package tarball (built via "mix hex.build").
+arcane-*.tar
+
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
+
diff --git a/examples/user_managment/phoenix_live_view_user_management/README.md b/examples/user_managment/phoenix_live_view_user_management/README.md
new file mode 100644
index 0000000..dcaa531
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/README.md
@@ -0,0 +1,75 @@
+# Supabase Phoenix LiveView User Management
+
+This repo is a quick sample of how you can get started building apps using [Phoenix LiveView](https://phoenixframework.org) and Supabase. You can find a step by step guide of how to build out this app in the [Quickstart: Phoenix LiveView guide](./guides/quickstart.md).
+
+This repo will demonstrate how to:
+
+- sign users in with Supabase Auth using [magic link](https://supabase.io/docs/reference/dart/auth-signin#sign-in-with-magic-link)
+- store and retrieve data with [Supabase database](https://supabase.io/docs/guides/database)
+- store image files in [Supabase storage](https://supabase.io/docs/guides/storage)
+
+## Getting Started
+
+Before running this app, you need to create a Supabase project and copy [your credentials](./guides/quickstart.md#get-the-api-keys) to `.env` or you can safely use [supabase-cli](https://supabase.com/docs/guides/cli/getting-started) and use the already defined `.env.dev`.
+
+Run the following command to launch it on `localhost:4000`
+
+```bash
+mix dev
+```
+
+> Note that this command `mix dev` is a custom alias defind on `mix.exs`.
+
+## Database Schema
+
+```sql
+-- Create a table for public "profiles"
+create table profiles (
+ id uuid references auth.users not null,
+ updated_at timestamp with time zone,
+ username text unique,
+ avatar_url text,
+ website text,
+
+ primary key (id),
+ unique(username),
+ constraint username_length check (char_length(username) >= 3)
+);
+
+alter table profiles enable row level security;
+
+create policy "Public profiles are viewable by everyone."
+ on profiles for select
+ using ( true );
+
+create policy "Users can insert their own profile."
+ on profiles for insert
+ with check ( (select auth.uid()) = id );
+
+create policy "Users can update own profile."
+ on profiles for update
+ using ( (select auth.uid()) = id );
+
+-- Set up Realtime!
+begin;
+ drop publication if exists supabase_realtime;
+ create publication supabase_realtime;
+commit;
+alter publication supabase_realtime add table profiles;
+
+-- Set up Storage!
+insert into storage.buckets (id, name)
+values ('avatars', 'avatars');
+
+create policy "Avatar images are publicly accessible."
+ on storage.objects for select
+ using ( bucket_id = 'avatars' );
+
+create policy "Anyone can upload an avatar."
+ on storage.objects for insert
+ with check ( bucket_id = 'avatars' );
+```
+
+> [!INFO]
+> You can find the SQL schema in the [migrations](./priv/repo/migrations) folder.
+> Generally you would prefer to use direct connection between your app (with ecto) and the Supabase postgres intances instead of using PostgREST
diff --git a/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css b/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css
new file mode 100644
index 0000000..4d45a34
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css
@@ -0,0 +1,424 @@
+@import "./flash.css";
+
+html,
+body {
+ --custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
+ Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
+ sans-serif;
+ --custom-bg-color: #101010;
+ --custom-panel-color: #222;
+ --custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8);
+ --custom-color: #fff;
+ --custom-color-brand: #24b47e;
+ --custom-color-secondary: #666;
+ --custom-border: 1px solid #333;
+ --custom-border-radius: 5px;
+ --custom-spacing: 5px;
+
+ padding: 0;
+ margin: 0;
+ font-family: var(--custom-font-family);
+ background-color: var(--custom-bg-color);
+}
+
+* {
+ color: var(--custom-color);
+ font-family: var(--custom-font-family);
+ box-sizing: border-box;
+}
+
+html,
+body,
+#__next {
+ height: 100vh;
+ width: 100vw;
+ overflow-x: hidden;
+}
+
+/* Grid */
+
+.container {
+ width: 90%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.row {
+ position: relative;
+ width: 100%;
+}
+
+.row [class^="col"] {
+ float: left;
+ margin: 0.5rem 2%;
+ min-height: 0.125rem;
+}
+
+.col-1,
+.col-2,
+.col-3,
+.col-4,
+.col-5,
+.col-6,
+.col-7,
+.col-8,
+.col-9,
+.col-10,
+.col-11,
+.col-12 {
+ width: 96%;
+}
+
+.col-1-sm {
+ width: 4.33%;
+}
+
+.col-2-sm {
+ width: 12.66%;
+}
+
+.col-3-sm {
+ width: 21%;
+}
+
+.col-4-sm {
+ width: 29.33%;
+}
+
+.col-5-sm {
+ width: 37.66%;
+}
+
+.col-6-sm {
+ width: 46%;
+}
+
+.col-7-sm {
+ width: 54.33%;
+}
+
+.col-8-sm {
+ width: 62.66%;
+}
+
+.col-9-sm {
+ width: 71%;
+}
+
+.col-10-sm {
+ width: 79.33%;
+}
+
+.col-11-sm {
+ width: 87.66%;
+}
+
+.col-12-sm {
+ width: 96%;
+}
+
+.row::after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.hidden-sm {
+ display: none;
+}
+
+@media only screen and (min-width: 33.75em) {
+ /* 540px */
+ .container {
+ width: 80%;
+ }
+}
+
+@media only screen and (min-width: 45em) {
+ /* 720px */
+ .col-1 {
+ width: 4.33%;
+ }
+
+ .col-2 {
+ width: 12.66%;
+ }
+
+ .col-3 {
+ width: 21%;
+ }
+
+ .col-4 {
+ width: 29.33%;
+ }
+
+ .col-5 {
+ width: 37.66%;
+ }
+
+ .col-6 {
+ width: 46%;
+ }
+
+ .col-7 {
+ width: 54.33%;
+ }
+
+ .col-8 {
+ width: 62.66%;
+ }
+
+ .col-9 {
+ width: 71%;
+ }
+
+ .col-10 {
+ width: 79.33%;
+ }
+
+ .col-11 {
+ width: 87.66%;
+ }
+
+ .col-12 {
+ width: 96%;
+ }
+
+ .hidden-sm {
+ display: block;
+ }
+}
+
+@media only screen and (min-width: 60em) {
+ /* 960px */
+ .container {
+ width: 75%;
+ max-width: 60rem;
+ }
+}
+
+/* Forms */
+
+label {
+ display: block;
+ margin: 5px 0;
+ color: var(--custom-color-secondary);
+ font-size: 0.8rem;
+ text-transform: uppercase;
+}
+
+input {
+ width: 100%;
+ border-radius: 5px;
+ border: var(--custom-border);
+ padding: 8px;
+ font-size: 0.9rem;
+ background-color: var(--custom-bg-color);
+ color: var(--custom-color);
+}
+
+input[disabled] {
+ color: var(--custom-color-secondary);
+}
+
+/* Utils */
+
+.block {
+ display: block;
+ width: 100%;
+}
+
+.inline-block {
+ display: inline-block;
+ width: 100%;
+}
+
+.flex {
+ display: flex;
+}
+
+.flex.column {
+ flex-direction: column;
+}
+
+.flex.row {
+ flex-direction: row;
+}
+
+.flex.flex-1 {
+ flex: 1 1 0;
+}
+
+.flex-end {
+ justify-content: flex-end;
+}
+
+.flex-center {
+ justify-content: center;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.text-sm {
+ font-size: 0.8rem;
+ font-weight: 300;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.font-light {
+ font-weight: 300;
+}
+
+.opacity-half {
+ opacity: 50%;
+}
+
+/* Button */
+
+button,
+.button {
+ color: var(--custom-color);
+ border: var(--custom-border);
+ background-color: var(--custom-bg-color);
+ display: inline-block;
+ text-align: center;
+ border-radius: var(--custom-border-radius);
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+ text-align: center;
+ font-size: 0.9rem;
+ text-transform: uppercase;
+}
+
+button.primary,
+.button.primary {
+ background-color: var(--custom-color-brand);
+ border: 1px solid var(--custom-color-brand);
+}
+
+/* Widgets */
+
+.card {
+ width: 100%;
+ display: block;
+ border: var(--custom-border);
+ border-radius: var(--custom-border-radius);
+ padding: var(--custom-spacing);
+}
+
+.avatar {
+ border-radius: var(--custom-border-radius);
+ overflow: hidden;
+ max-width: 100%;
+}
+
+.avatar.image {
+ object-fit: cover;
+}
+
+.avatar.no-image {
+ background-color: #333;
+ border: 1px solid rgb(200, 200, 200);
+ border-radius: 5px;
+}
+
+.footer {
+ position: absolute;
+ max-width: 100%;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ flex-flow: row;
+ border-top: var(--custom-border);
+ background-color: var(--custom-bg-color);
+}
+
+.footer div {
+ padding: var(--custom-spacing);
+ display: flex;
+ align-items: center;
+ width: 100%;
+}
+
+.footer div > img {
+ height: 20px;
+ margin-left: 10px;
+}
+
+.footer > div:first-child {
+ display: none;
+}
+
+.footer > div:nth-child(2) {
+ justify-content: left;
+}
+
+@media only screen and (min-width: 60em) {
+ /* 960px */
+ .footer > div:first-child {
+ display: flex;
+ }
+
+ .footer > div:nth-child(2) {
+ justify-content: center;
+ }
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.mainHeader {
+ width: 100%;
+ font-size: 1.3rem;
+ margin-bottom: 20px;
+}
+
+.avatarPlaceholder {
+ border: var(--custom-border);
+ border-radius: var(--custom-border-radius);
+ width: 35px;
+ height: 35px;
+ background-color: rgba(255, 255, 255, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.form-widget {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.form-widget > .button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background-color: #444444;
+ text-transform: none !important;
+ transition: all 0.2s ease;
+}
+
+.form-widget .button:hover {
+ background-color: #2a2a2a;
+}
+
+.form-widget .button > .loader {
+ width: 17px;
+ animation: spin 1s linear infinite;
+ filter: invert(1);
+}
diff --git a/examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css b/examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css
new file mode 100644
index 0000000..02a52a0
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css
@@ -0,0 +1,117 @@
+/* Flash Container Base Styles */
+.flash-container {
+ position: fixed;
+ top: 0.5rem; /* Equivalent to top-2 */
+ right: 0.5rem; /* Equivalent to right-2 */
+ margin-right: 0.5rem; /* Equivalent to mr-2 */
+ width: 20rem; /* Equivalent to w-80 */
+ z-index: 50; /* Equivalent to z-50 */
+ border-radius: 0.5rem; /* Equivalent to rounded-lg */
+ padding: 0.75rem; /* Equivalent to p-3 */
+ border: 1px solid; /* Equivalent to ring-1 */
+ display: flex;
+ flex-direction: column;
+ background-color: var(--flash-background);
+ color: var(--flash-text);
+ border-color: var(--flash-border);
+ fill: var(--flash-fill);
+}
+
+/* Responsive Width for Larger Screens */
+@media (min-width: 640px) {
+ /* sm:w-96 */
+ .flash-container {
+ width: 24rem; /* Equivalent to sm:w-96 */
+ }
+}
+
+/* Flash Variants */
+
+/* Info Flash Styles */
+.flash-info {
+ --flash-background: #f0fff4; /* bg-emerald-50 */
+ --flash-text: #065f46; /* text-emerald-800 */
+ --flash-border: #10b981; /* ring-emerald-500 */
+ --flash-fill: #06b6d4; /* fill-cyan-900 */
+}
+
+/* Error Flash Styles */
+.flash-error {
+ --flash-background: #fff1f2; /* bg-rose-50 */
+ --flash-text: #991b1b; /* text-rose-900 */
+ --flash-border: #f43f5e; /* ring-rose-500 */
+ --flash-fill: #991b1b; /* fill-rose-900 */
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* shadow-md */
+}
+
+/* Flash Title Styles */
+.flash-title {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem; /* Equivalent to gap-1.5 */
+ font-size: 0.875rem; /* Equivalent to text-sm */
+ font-weight: 600; /* Equivalent to font-semibold */
+ line-height: 1.5rem; /* Equivalent to leading-6 */
+}
+
+/* Icon Styles */
+.icon-small {
+ height: 1rem; /* Equivalent to h-4 */
+ width: 1rem; /* Equivalent to w-4 */
+}
+
+.icon-medium {
+ height: 1.25rem; /* Equivalent to h-5 */
+ width: 1.25rem; /* Equivalent to w-5 */
+}
+
+/* Flash Message Text Styles */
+.flash-message {
+ margin-top: 0.5rem; /* Equivalent to mt-2 */
+ font-size: 0.875rem; /* Equivalent to text-sm */
+ line-height: 1.25rem; /* Equivalent to leading-5 */
+}
+
+/* Close Button Styles */
+.flash-close-button {
+ position: absolute;
+ top: 0.25rem; /* Equivalent to top-1 */
+ right: 0.25rem; /* Equivalent to right-1 */
+ padding: 0.5rem; /* Equivalent to p-2 */
+ background: none;
+ border: none;
+ cursor: pointer;
+ opacity: 0.4; /* Equivalent to opacity-40 */
+ transition: opacity 0.2s ease-in-out;
+}
+
+.flash-close-button:hover {
+ opacity: 0.7; /* Equivalent to group-hover:opacity-70 */
+}
+
+/* Flash Group Container */
+.flash-group-container {
+ /* Add any specific styles for the flash group container if needed */
+}
+
+/* Animated Spin for Icons */
+.icon-animated-spin {
+ /* Apply spin only if user has not requested reduced motion */
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .icon-animated-spin {
+ animation: spin 1s linear infinite; /* Equivalent to animate-spin */
+ }
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* Hidden Utility Class */
+.hidden {
+ display: none;
+}
diff --git a/examples/user_managment/phoenix_live_view_user_management/assets/js/app.js b/examples/user_managment/phoenix_live_view_user_management/assets/js/app.js
new file mode 100644
index 0000000..7047842
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/assets/js/app.js
@@ -0,0 +1,58 @@
+import "../css/app.css";
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html";
+// Establish Phoenix Socket and LiveView configuration.
+import { Socket } from "phoenix";
+import { LiveSocket } from "phoenix_live_view";
+import topbar from "../vendor/topbar";
+
+let Hooks = {};
+
+Hooks.LivePreview = {
+ mounted() {
+ this.handleEvent("consume-blob", ({ blob }) => {
+ const url = URL.createObjectURL(blob);
+ this.pushEvent("avatar-blob-url", { url });
+ });
+ },
+};
+
+let csrfToken = document
+ .querySelector("meta[name='csrf-token']")
+ .getAttribute("content");
+let liveSocket = new LiveSocket("/live", Socket, {
+ longPollFallbackMs: 2500,
+ params: { _csrf_token: csrfToken },
+ hooks: Hooks,
+});
+
+// Show progress bar on live navigation and form submits
+topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
+window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
+window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());
+
+// connect if there are any LiveViews on the page
+liveSocket.connect();
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+window.liveSocket = liveSocket;
diff --git a/examples/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js b/examples/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js
new file mode 100644
index 0000000..4195727
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js
@@ -0,0 +1,165 @@
+/**
+ * @license MIT
+ * topbar 2.0.0, 2023-02-04
+ * https://buunguyen.github.io/topbar
+ * Copyright (c) 2021 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ // https://gist.github.com/paulirish/1579671
+ (function () {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame =
+ window[vendors[x] + "RequestAnimationFrame"];
+ window.cancelAnimationFrame =
+ window[vendors[x] + "CancelAnimationFrame"] ||
+ window[vendors[x] + "CancelRequestAnimationFrame"];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ })();
+
+ var canvas,
+ currentProgress,
+ showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ document.body.appendChild(canvas);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ show: function (delay) {
+ if (showing) return;
+ if (delay) {
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), delay);
+ } else {
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/examples/user_managment/phoenix_live_view_user_management/config/config.exs b/examples/user_managment/phoenix_live_view_user_management/config/config.exs
new file mode 100644
index 0000000..e0f6aac
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/config/config.exs
@@ -0,0 +1,50 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+config :arcane, Arcane.Supabase.Client,
+ base_url: System.get_env("SUPABASE_URL"),
+ api_key: System.get_env("SUPABASE_KEY"),
+ db: %{schema: "public"}
+
+config :arcane,
+ ecto_repos: [Arcane.Repo],
+ generators: [timestamp_type: :utc_datetime]
+
+# Configures the endpoint
+config :arcane, ArcaneWeb.Endpoint,
+ url: [host: "localhost"],
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [
+ formats: [html: ArcaneWeb.ErrorHTML, json: ArcaneWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: Arcane.PubSub,
+ live_view: [signing_salt: "pNdlrcKe"]
+
+# Configure esbuild (the version is required)
+config :esbuild,
+ version: "0.17.11",
+ arcane: [
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+ ]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/examples/user_managment/phoenix_live_view_user_management/config/dev.exs b/examples/user_managment/phoenix_live_view_user_management/config/dev.exs
new file mode 100644
index 0000000..1d9fcc4
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/config/dev.exs
@@ -0,0 +1,81 @@
+import Config
+
+# Supabase local database config
+config :arcane, Arcane.Repo,
+ username: System.get_env("DATABASE_USER"),
+ password: System.get_env("DATABASE_PASS"),
+ hostname: System.get_env("DATABASE_HOST"),
+ database: System.get_env("DATABASE_NAME"),
+ port: System.get_env("DATABASE_PORT"),
+ stacktrace: true,
+ show_sensitive_data_on_connection_error: true,
+ pool_size: 10
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
+config :arcane, ArcaneWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "nODI+64yZkWryU/7nSc+AmNiiaMj1hf4lXch5aFuf0UAoBO6jWZ6HN8zKs2npKD0",
+ watchers: [
+ esbuild: {Esbuild, :install_and_run, [:arcane, ~w(--sourcemap=inline --watch)]}
+ ]
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Watch static and templates for browser reloading.
+config :arcane, ArcaneWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"lib/arcane_web/(controllers|live|components)/.*(ex|heex)$"
+ ]
+ ]
+
+# Enable dev routes for dashboard and mailbox
+config :arcane, dev_routes: true
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+config :phoenix_live_view,
+ # Include HEEx debug annotations as HTML comments in rendered markup
+ debug_heex_annotations: true,
+ # Enable helpful, but potentially expensive runtime checks
+ enable_expensive_runtime_checks: true
diff --git a/examples/user_managment/phoenix_live_view_user_management/config/prod.exs b/examples/user_managment/phoenix_live_view_user_management/config/prod.exs
new file mode 100644
index 0000000..21aa7bd
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/config/prod.exs
@@ -0,0 +1,14 @@
+import Config
+
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix assets.deploy` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :arcane, ArcaneWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/examples/user_managment/phoenix_live_view_user_management/config/runtime.exs b/examples/user_managment/phoenix_live_view_user_management/config/runtime.exs
new file mode 100644
index 0000000..ffb9c02
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/config/runtime.exs
@@ -0,0 +1,99 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/arcane start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :arcane, ArcaneWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ database_url =
+ System.get_env("DATABASE_URL") ||
+ raise """
+ environment variable DATABASE_URL is missing.
+ For example: ecto://USER:PASS@HOST/DATABASE
+ """
+
+ maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
+
+ config :arcane, Arcane.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
+ socket_options: maybe_ipv6
+
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :arcane, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
+
+ config :arcane, ArcaneWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :arcane, ArcaneWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your config/prod.exs,
+ # ensuring no data is ever sent via http, always redirecting to https:
+ #
+ # config :arcane, ArcaneWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/config/test.exs b/examples/user_managment/phoenix_live_view_user_management/config/test.exs
new file mode 100644
index 0000000..7e7ea8a
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/config/test.exs
@@ -0,0 +1,32 @@
+import Config
+
+# Configure your database
+#
+# The MIX_TEST_PARTITION environment variable can be used
+# to provide built-in test partitioning in CI environment.
+# Run `mix help test` for more information.
+config :arcane, Arcane.Repo,
+ username: "postgres",
+ password: "postgres",
+ hostname: "localhost",
+ database: "arcane_test#{System.get_env("MIX_TEST_PARTITION")}",
+ port: 54322,
+ pool: Ecto.Adapters.SQL.Sandbox,
+ pool_size: System.schedulers_online() * 2
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :arcane, ArcaneWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "W0RjQEaGhgYhQRvuaYSomPIeJ9iJLUtDVgMn3yJvwC9By/R9jpThnEdXxngvGQLG",
+ server: false
+
+# Print only warnings and errors during test
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Enable helpful, but potentially expensive runtime checks
+config :phoenix_live_view,
+ enable_expensive_runtime_checks: true
diff --git a/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md b/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md
new file mode 100644
index 0000000..f2cb4c3
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md
@@ -0,0 +1,128 @@
+# Build a User Management App with Phoenix LiveView
+
+Learn how to use Supabase in your Phoenix LiveView App.
+
+This tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses:
+
+- [Supabase Database](https://supabase.com/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](https://supabase.com/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.
+- [Supabase Auth](https://supabase.com/docs/guides/auth) - allow users to sign up and log in.
+- [Supabase Storage](https://supabase.com/docs/guides/storage) - users can upload a profile photo.
+
+![Supabase User Management example](https://supabase.com/docs/img/user-management-demo.png)
+
+> [!INFO]
+> If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/zoedsoupe/supabase-ex/tree/main/examples/user_management/phoenix_live_view_user_management).
+
+## Project Setup
+
+Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database.
+
+### Create a Project
+
+1. [Create a new project](https://supabase.com/dashboard) in the Supabase Dashboard.
+2. Enter your project details.
+3. Wait for the new database to launch.
+
+### Set up the database schema
+
+I'll be doing that using the [Ecto migrations](https://hexdocs.pm/ecto_sql), but you can also do that manually in the Supabase Dashboard.
+
+### Get the API Keys
+
+Now that you've created some database tables, you are ready to insert data using the auto-generated API. We just need to get the Project URL and `anon` key from the API settings.
+
+1. Go to the [API Settings](https://supabase.com/dashboard/project/_/settings/api) page in the Dashboard.
+2. Find your Project `URL`, `anon`, and `service_role` keys on this page.
+
+## Building the app
+
+Let's start building the Phoenix LiveView app from scratch.
+
+### Initialize a Phoenix LiveView app
+
+We can use [`mix phx.new`](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html) to create an app called `arcane`:
+
+> Before issuing this command, ensure you have [elixir](https://elixir-lang.org) installed
+> Also ensure that you have the [phoenix installer](https://hexdocs.pm/phoenix/installation.html) in your machine
+
+```bash
+mix phx.new --adapter bandit --no-tailwind --app arcane phoenix_live_view_user_management
+
+cd phoenix_live_view_user_management
+```
+
+Then let's install the needed dependencies to integrate with supabase: [Supabase Potion](https://hexdocs.pm/supabase_potion). We only need to add these lines to your `deps` in `mix.exs`:
+
+```elixir
+defp deps do
+ [
+ {:supabase_potion, "~> 0.5"},
+ {:supabase_gotrue, "~> 0.3"},
+ {:supabase_storage, "~> 0.3"},
+ # other dependencies
+ ]
+end
+```
+
+Then install them with:
+
+```sh
+mix deps.get
+```
+
+And finally we want to save the environment variables in a `.env`.
+All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
+
+```bash .env
+export SUPABASE_URL="YOUR_SUPABASE_URL"
+export SUPABASE_KEY="YOUR_SUPABASE_ANON_KEY"
+```
+
+These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
+Amazing thing about [NuxtSupabase](https://supabase.nuxtjs.org/) is that setting environment variables is all we need to do in order to start using Supabase.
+No need to initialize Supabase. The library will take care of it automatically.
+
+### App styling (optional)
+
+An optional step is to update the CSS file `assets/main.css` to make the app look nice.
+You can find the full contents of this file [here](https://github.com/zoedsoupe/supabase-ex/blob/main/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css).
+
+### Set up Auth component
+
+TODO
+
+### User state
+
+TODO
+
+### Account component
+
+TODO
+
+### Launch!
+
+TODO
+
+Once that's done, run this in a terminal window:
+
+```bash
+iex -S mix phx.server
+```
+
+And then open the browser to [localhost:4000](http://localhost:4000) and you should see the completed app.
+
+![Supabase Phoenix LiveView](https://supabase.com/docs/img/supabase-vue-3-demo.png)
+
+## Bonus: Profile photos
+
+Every Supabase project is configured with [Storage](https://supabase.com//docs/guides/storage) for managing large files like photos and videos.
+
+### Create an upload widget
+
+TODO
+
+### Add the new widget
+
+TODO
+
+That is it! You should now be able to upload a profile photo to Supabase Storage and you have a fully functional application.
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex
new file mode 100644
index 0000000..094de16
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex
@@ -0,0 +1,9 @@
+defmodule Arcane do
+ @moduledoc """
+ Arcane keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex
new file mode 100644
index 0000000..5d16ede
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex
@@ -0,0 +1,35 @@
+defmodule Arcane.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ ArcaneWeb.Telemetry,
+ Arcane.Repo,
+ {DNSCluster, query: Application.get_env(:arcane, :dns_cluster_query) || :ignore},
+ {Phoenix.PubSub, name: Arcane.PubSub},
+ # Start a worker by calling: Arcane.Worker.start_link(arg)
+ # {Arcane.Worker, arg},
+ # Start to serve requests, typically the last entry
+ ArcaneWeb.Endpoint,
+ Arcane.Supabase.Client
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: Arcane.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ ArcaneWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex
new file mode 100644
index 0000000..c72c126
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex
@@ -0,0 +1,32 @@
+defmodule Arcane.Profiles do
+ import Ecto.Query
+
+ alias Arcane.Profiles.Profile
+ alias Arcane.Repo
+
+ def get_profile(id: id) do
+ Repo.get(Profile, id)
+ end
+
+ def create_profile(user_id: user_id) do
+ changeset = Profile.changeset(%Profile{}, %{id: user_id})
+ Repo.insert(changeset, on_conflict: :nothing, conflict_target: [:id])
+ end
+
+ def update_profile(%{"id" => profile_id} = attrs) do
+ changeset = Profile.update_changeset(attrs)
+
+ if changeset.valid? do
+ updated_at = NaiveDateTime.utc_now()
+ changes = [{:updated_at, updated_at} | Map.to_list(changeset.changes)]
+ q = from p in Profile, where: p.id == ^profile_id, select: p
+
+ case Repo.update_all(q, set: changes) do
+ {1, [profile]} -> {:ok, profile}
+ _ -> {:error, :failed_to_update_profile}
+ end
+ else
+ {:error, changeset}
+ end
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex
new file mode 100644
index 0000000..071eb52
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex
@@ -0,0 +1,44 @@
+defmodule Arcane.Profiles.Profile do
+ @moduledoc """
+ Profiles are the main data structure for users.
+ """
+
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ @type t :: %__MODULE__{
+ id: Ecto.UUID.t(),
+ username: String.t() | nil,
+ website: String.t() | nil,
+ avatar_url: String.t() | nil,
+ inserted_at: NaiveDateTime.t(),
+ updated_at: NaiveDateTime.t()
+ }
+
+ @primary_key {:id, :binary_id, autogenerate: false}
+ schema "profiles" do
+ field :username, :string
+ field :website, :string
+ field :avatar_url, :string
+
+ timestamps()
+ end
+
+ def changeset(profile \\ %__MODULE__{}, %{} = params) do
+ profile
+ |> cast(params, [:id, :username, :website, :avatar_url])
+ |> validate_required([:id])
+ |> validate_length(:username, min: 3)
+ |> validate_length(:website, max: 255)
+ |> unique_constraint(:username)
+ |> foreign_key_constraint(:id)
+ end
+
+ def update_changeset(%{} = params) do
+ %__MODULE__{}
+ |> cast(params, [:username, :website, :avatar_url])
+ |> validate_length(:username, min: 3)
+ |> validate_length(:website, max: 255)
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex
new file mode 100644
index 0000000..ff32a94
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex
@@ -0,0 +1,5 @@
+defmodule Arcane.Repo do
+ use Ecto.Repo,
+ otp_app: :arcane,
+ adapter: Ecto.Adapters.Postgres
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex
new file mode 100644
index 0000000..230bf25
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex
@@ -0,0 +1,3 @@
+defmodule Arcane.Supabase.Client do
+ use Supabase.Client, otp_app: :arcane
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex
new file mode 100644
index 0000000..b5f1643
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex
@@ -0,0 +1,111 @@
+defmodule ArcaneWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, components, channels, and so on.
+
+ This can be used in your application as:
+
+ use ArcaneWeb, :controller
+ use ArcaneWeb, :html
+
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define additional modules and import
+ those modules here.
+ """
+
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller,
+ formats: [:html, :json],
+ layouts: [html: ArcaneWeb.Layouts]
+
+ import Plug.Conn
+
+ unquote(verified_routes())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {ArcaneWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # UI components
+ import ArcaneWeb.Components
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
+ end
+ end
+
+ def verified_routes do
+ quote do
+ use Phoenix.VerifiedRoutes,
+ endpoint: ArcaneWeb.Endpoint,
+ router: ArcaneWeb.Router,
+ statics: ArcaneWeb.static_paths()
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/live_view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex
new file mode 100644
index 0000000..47a499d
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex
@@ -0,0 +1,17 @@
+defmodule ArcaneWeb.Auth do
+ use Supabase.GoTrue.LiveView,
+ endpoint: ArcaneWeb.Endpoint,
+ client: Arcane.Supabase.Client,
+ signed_in_path: "/",
+ not_authenticated_path: "/"
+
+ # LiveView cannot write cookies
+ # or set session, so we need to use Plug
+ # to handle the session and cookies
+ # check ArcaneWeb.SessionController
+ use Supabase.GoTrue.Plug,
+ endpoint: ArcaneWeb.Endpoint,
+ client: Arcane.Supabase.Client,
+ signed_in_path: "/",
+ not_authenticated_path: "/"
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex
new file mode 100644
index 0000000..5067d52
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex
@@ -0,0 +1,124 @@
+defmodule ArcaneWeb.Components do
+ @moduledoc """
+ This module define function components.
+ """
+
+ use ArcaneWeb, :verified_routes
+ use Phoenix.Component
+
+ attr :upload, Phoenix.LiveView.UploadConfig, required: true
+ attr :size, :integer
+
+ def avatar(%{size: size} = assigns) do
+ size_str = "height: #{size}em; width: #{size}em;"
+ assigns = assign(assigns, size: size_str)
+
+ ~H"""
+
+ <.live_img_preview
+ :for={entry <- @upload.entries}
+ entry={entry}
+ alt="Avatar"
+ class="avatar-image"
+ style={@size}
+ />
+
+
+
+
+ Upload
+
+ <.live_file_input
+ upload={@upload}
+ id="single"
+ style="position: absolute; visibility: hidden;"
+ />
+
+
+ """
+ end
+
+ attr :form, Phoenix.HTML.Form, required: true
+
+ def auth(assigns) do
+ ~H"""
+ <.form for={@form} action={~p"/session"} class="row flex flex-center">
+
+
+ """
+ end
+
+ attr :form, Phoenix.HTML.Form, required: true
+
+ @doc """
+ We actually need 2 different forms as the first one will keep track of
+ the profile update data and emit LiveView events and the second one will submit an HTTP request
+ `DELETE /session` to log out the current user (aka delete session cookies)
+ """
+ def account(assigns) do
+ ~H"""
+ <.form for={@form} class="form-widget" phx-submit="update-profile" phx-change="upload-profile">
+
+
+ Email
+
+
+
+ Name
+
+
+
+ Website
+
+
+
+
+
+ Update
+
+
+
+ <.form for={%{}} action={~p"/session"} method="delete">
+
+
+ Sign Out
+
+
+
+ """
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex
new file mode 100644
index 0000000..3992fb9
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex
@@ -0,0 +1,14 @@
+defmodule ArcaneWeb.Layouts do
+ @moduledoc """
+ This module holds different layouts used by your application.
+
+ See the `layouts` directory for all templates available.
+ The "root" layout is a skeleton rendered as part of the
+ application router. The "app" layout is set as the default
+ layout on both `use ArcaneWeb, :controller` and
+ `use ArcaneWeb, :live_view`.
+ """
+ use ArcaneWeb, :html
+
+ embed_templates "layouts/*"
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex
new file mode 100644
index 0000000..f81f065
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex
@@ -0,0 +1,4 @@
+
+
+ <%= @inner_content %>
+
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex
new file mode 100644
index 0000000..7c0c7c3
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ <.live_title>
+ <%= assigns[:page_title] || "Arcane" %>
+
+
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex
new file mode 100644
index 0000000..0e247e5
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex
@@ -0,0 +1,24 @@
+defmodule ArcaneWeb.ErrorHTML do
+ @moduledoc """
+ This module is invoked by your endpoint in case of errors on HTML requests.
+
+ See config/config.exs.
+ """
+ use ArcaneWeb, :html
+
+ # If you want to customize your error pages,
+ # uncomment the embed_templates/1 call below
+ # and add pages to the error directory:
+ #
+ # * lib/arcane_web/controllers/error_html/404.html.heex
+ # * lib/arcane_web/controllers/error_html/500.html.heex
+ #
+ # embed_templates "error_html/*"
+
+ # The default is to render a plain text page based on
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex
new file mode 100644
index 0000000..2c59dc0
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex
@@ -0,0 +1,21 @@
+defmodule ArcaneWeb.ErrorJSON do
+ @moduledoc """
+ This module is invoked by your endpoint in case of errors on JSON requests.
+
+ See config/config.exs.
+ """
+
+ # If you want to customize a particular status code,
+ # you may add your own clauses, such as:
+ #
+ # def render("500.json", _assigns) do
+ # %{errors: %{detail: "Internal Server Error"}}
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.json" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex
new file mode 100644
index 0000000..58fd526
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex
@@ -0,0 +1,96 @@
+defmodule ArcaneWeb.SessionController do
+ use ArcaneWeb, :controller
+
+ import ArcaneWeb.Auth
+ import Phoenix.LiveView.Controller
+
+ alias Arcane.Profiles
+ alias ArcaneWeb.UserManagementLive
+ alias Supabase.GoTrue
+
+ require Logger
+
+ @doc """
+ THis function is responsible to process the log in request and send tbe
+ magic link via Supabase/GoTrue
+
+ Note that we do `live_render` since there's no state to mantain between
+ controller and the live view itself (that will do authentication checks).
+ """
+ def create(conn, %{"email" => email}) do
+ params = %{
+ email: email,
+ options: %{
+ should_create_user: true,
+ email_redirect_to: ~p"/session/confirm"
+ }
+ }
+
+ {:ok, client} = Arcane.Supabase.Client.get_client()
+
+ case GoTrue.sign_in_with_otp(client, params) do
+ :ok ->
+ live_render(conn, UserManagementLive)
+
+ {:error, error} ->
+ Logger.error("""
+ [#{__MODULE__}] => Failed to login user:
+ ERROR: #{inspect(error, pretty: true)}
+ """)
+
+ live_render(conn, UserManagementLive)
+ end
+ end
+
+ @doc """
+ Once the user clicks the email link that they'll receive, the link will redirect
+ to the `/session/confirm` route defined on `ArcaneWeb.Router` and will trigger
+ this function.
+
+ So we create an empty Profile for this user, so the `UserManagementLive` can
+ correctly show informations about the profile.
+
+ Note also that we put the token into the session, as configured in the `ArcaneWeb.Endpoint`
+ it will set up session cookies to store authentication information locally.
+
+ Finally, we redirect back the user to the root page, that will redenr `UserManagementLive`
+ live view. We could use `live_render`, but it would need to pass all the state and session
+ mannually to the live view, which is unecessary here since it will happen automatically on
+ `mount` of the live view.
+ """
+ def confirm(conn, %{"token_hash" => token_hash, "type" => "magiclink"}) do
+ {:ok, client} = Arcane.Supabase.Client.get_client()
+
+ params = %{
+ token_hash: token_hash,
+ type: :magiclink
+ }
+
+ with {:ok, session} <- GoTrue.verify_otp(client, params),
+ {:ok, user} <- GoTrue.get_user(client, session) do
+ Profiles.create_profile(user_id: user.id)
+
+ conn
+ |> put_token_in_session(session.access_token)
+ |> redirect(to: ~p"/")
+ else
+ {:error, error} ->
+ Logger.error("""
+ [#{__MODULE__}] => Failed to verify OTP:
+ ERROR: #{inspect(error, pretty: true)}
+ """)
+
+ redirect(conn, to: ~p"/")
+ end
+ end
+
+ @doc """
+ This function clears the local session, which includes the session cookie, so the user
+ will need to authenticate again on the application.
+ """
+ def signout(conn, _params) do
+ conn
+ |> log_out_user(:local)
+ |> live_render(UserManagementLive)
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex
new file mode 100644
index 0000000..c36476d
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex
@@ -0,0 +1,49 @@
+defmodule ArcaneWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :arcane
+
+ # The session will be stored in the cookie and signed,
+ # this means its contents can be read but not tampered with.
+ # Set :encryption_salt if you would also like to encrypt it.
+ @session_options [
+ store: :cookie,
+ key: "_arcane_key",
+ signing_salt: "il6sVVfc",
+ same_site: "Lax"
+ ]
+
+ socket "/live", Phoenix.LiveView.Socket,
+ websocket: [connect_info: [session: @session_options]],
+ longpoll: [connect_info: [session: @session_options]]
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # You should set gzip to true if you are running phx.digest
+ # when deploying your static files in production.
+ plug Plug.Static,
+ at: "/",
+ from: :arcane,
+ gzip: false,
+ only: ArcaneWeb.static_paths()
+
+ # Code reloading can be explicitly enabled under the
+ # :code_reloader configuration of your endpoint.
+ if code_reloading? do
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ plug Phoenix.Ecto.CheckRepoStatus, otp_app: :arcane
+ end
+
+ plug Plug.RequestId
+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+ plug Plug.Session, @session_options
+ plug ArcaneWeb.Router
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex
new file mode 100644
index 0000000..80c697c
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex
@@ -0,0 +1,170 @@
+defmodule ArcaneWeb.UserManagementLive do
+ use ArcaneWeb, :live_view
+
+ import ArcaneWeb.Components
+
+ alias Arcane.Profiles
+ alias Phoenix.LiveView.AsyncResult
+ alias Supabase.Storage
+ alias Supabase.Storage.Bucket
+
+ require Logger
+
+ on_mount {ArcaneWeb.Auth, :mount_current_user}
+
+ @bucket_name "avatars"
+
+ def mount(_params, _session, socket) do
+ current_user = socket.assigns.current_user
+ profile = current_user && Profiles.get_profile(id: current_user.id)
+ account_form = make_account_form(profile, current_user)
+
+ # `assigns` on render expect that the
+ # `@` is defined on `socket.assigns`
+ # so we need to define it here if there isn't
+ # any current user
+ {:ok,
+ socket
+ |> assign(:page_title, "User Management")
+ |> assign(:auth_form, to_form(%{"email" => nil}))
+ |> assign(:account_form, account_form)
+ |> assign(:profile, profile)
+ |> allow_upload(:avatar,
+ auto_upload: true,
+ accept: ["image/*"],
+ progress: &handle_progress/3
+ )
+ |> assign(:avatar_blob, AsyncResult.loading())
+ |> start_async(:download_avatar_blob, fn -> maybe_download_avatar(profile) end)}
+ end
+
+ def render(assigns) do
+ ~H"""
+
+ <.avatar :if={@current_user} upload={@uploads.avatar} size={10} />
+ <.account :if={@current_user} form={@account_form} />
+ <.auth :if={is_nil(@current_user)} form={@auth_form} />
+
+ """
+ end
+
+ def handle_event("update-profile", params, socket) do
+ current_user = socket.assigns.current_user
+ params = Map.merge(params, %{"id" => current_user.id})
+
+ case Profiles.update_profile(params) do
+ {:ok, profile} ->
+ Logger.info("""
+ [#{__MODULE__}] => Profile updated: #{inspect(profile)}
+ """)
+
+ account_form = make_account_form(profile, current_user)
+ {:noreply, assign(socket, :account_form, account_form)}
+
+ {:error, error} ->
+ Logger.error("""
+ [#{__MODULE__}] => Error updating profile: #{inspect(error)}
+ """)
+
+ {:noreply, put_flash(socket, :error, "Error updating profile")}
+ end
+ end
+
+ def handle_event("avatar-blob-url", %{"url" => url}, socket) do
+ {:noreply, assign(socket, avatar: url)}
+ end
+
+ def handle_event("sign-out", _params, socket) do
+ ArcaneWeb.Auth.log_out_user(socket, :local)
+ {:noreply, socket}
+ end
+
+ # fallback to avoid crashing the LiveView process
+ # although this isn't a problem for Phoenix
+ # as Elixir is fault tolerant, but it helps with observability
+ def handle_event(event, params, socket) do
+ Logger.info("""
+ [#{__MODULE__}] => Unhandled event: #{event}
+ PARAMS: #{inspect(params, pretty: true)}
+ """)
+
+ {:noreply, socket}
+ end
+
+ def handle_async(:download_avatar_blob, {:ok, nil}, socket) do
+ avatar_blob = socket.assigns.avatar_blob
+ ok = AsyncResult.ok(avatar_blob, nil)
+ {:noreply, assign(socket, avatar_blob: ok)}
+ end
+
+ def handle_async(:download_avatar_blob, {:ok, blob}, socket) do
+ avatar_blob = socket.assigns.avatar_blob
+
+ {:noreply,
+ socket
+ |> assign(avatar_blob: AsyncResult.ok(avatar_blob, blob))
+ |> push_event("consume-blob", %{blob: blob})}
+ end
+
+ def handle_async(:download_avatar_blob, {:error, error}, socket) do
+ Logger.error("""
+ [#{__MODULE__}] => Error downloading avatar blob: #{inspect(error)}
+ """)
+
+ avatar_blob = socket.assigns.avatar_blob
+ failed = AsyncResult.failed(avatar_blob, {:error, error})
+ {:noreply, assign(socket, avatar_blob: failed)}
+ end
+
+ defp maybe_download_avatar(nil), do: nil
+ defp maybe_download_avatar(%Profiles.Profile{avatar_url: nil}), do: nil
+
+ defp maybe_download_avatar(%Profiles.Profile{} = profile) do
+ {:ok, client} = Arcane.Supabase.Client.get_client()
+ bucket = %Bucket{name: @bucket_name}
+
+ Storage.download_object(client, bucket, profile.avatar_url)
+ end
+
+ defp make_account_form(profile, current_user) do
+ to_form(%{
+ "id" => profile && profile.id,
+ "username" => profile && profile.username,
+ "website" => profile && profile.website,
+ "email" => current_user && current_user.email,
+ "avatar" => nil
+ })
+ end
+
+ defp handle_progress(:avatar, entry, socket) when entry.done? do
+ current_user = socket.assigns.current_user
+ profile = socket.assigns.profile
+ params = %{profile: profile, user: current_user}
+ consume_uploaded_entry(socket, entry, &handle_avatar_upload(&1, params))
+
+ {:noreply, socket}
+ end
+
+ defp handle_progress(:avatar, entry, socket) do
+ Logger.info("[#{__MODULE__}] => Avatar with #{entry.progress} progress")
+ {:noreply, socket}
+ end
+
+ defp handle_avatar_upload(%{path: path}, %{user: current_user, profile: profile}) do
+ bucket = %Bucket{name: @bucket_name}
+ basename = Path.basename(path)
+ remote_path = Path.join([bucket.name, current_user.id, basename])
+ expires = :timer.hours(24) * 365
+
+ with {:ok, client} = Arcane.Supabase.Client.get_client(),
+ {:ok, obj} <- Supabase.Storage.upload_object(client, bucket, remote_path, path),
+ {:ok, url} <- Supabase.Storage.create_signed_url(client, bucket, remote_path, expires),
+ {:ok, _} <- Profiles.update_profile(%{id: profile.id, avatar_url: url}) do
+ {:ok, obj.path}
+ else
+ err ->
+ Logger.error("[#{__MODULE__}] => Failed to upload avatar with #{inspect(err)}")
+ err
+ end
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex
new file mode 100644
index 0000000..928c864
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex
@@ -0,0 +1,31 @@
+defmodule ArcaneWeb.Router do
+ use ArcaneWeb, :router
+
+ import ArcaneWeb.Auth
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_live_flash
+ plug :put_root_layout, html: {ArcaneWeb.Layouts, :root}
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ plug :fetch_current_user
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", ArcaneWeb do
+ pipe_through :browser
+
+ live "/", UserManagementLive
+
+ scope "/session" do
+ delete "/", SessionController, :signout
+ post "/", SessionController, :create
+ get "/confirm", SessionController, :confirm
+ end
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex
new file mode 100644
index 0000000..a846b7a
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex
@@ -0,0 +1,92 @@
+defmodule ArcaneWeb.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ # Telemetry poller will execute the given period measurements
+ # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.start.system_time",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.start.system_time",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.exception.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.socket_connected.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_joined.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_handled_in.duration",
+ tags: [:event],
+ unit: {:native, :millisecond}
+ ),
+
+ # Database Metrics
+ summary("arcane.repo.query.total_time",
+ unit: {:native, :millisecond},
+ description: "The sum of the other measurements"
+ ),
+ summary("arcane.repo.query.decode_time",
+ unit: {:native, :millisecond},
+ description: "The time spent decoding the data received from the database"
+ ),
+ summary("arcane.repo.query.query_time",
+ unit: {:native, :millisecond},
+ description: "The time spent executing the query"
+ ),
+ summary("arcane.repo.query.queue_time",
+ unit: {:native, :millisecond},
+ description: "The time spent waiting for a database connection"
+ ),
+ summary("arcane.repo.query.idle_time",
+ unit: {:native, :millisecond},
+ description:
+ "The time the connection spent waiting before being checked out for the query"
+ ),
+
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {ArcaneWeb, :count_users, []}
+ ]
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/mix.exs b/examples/user_managment/phoenix_live_view_user_management/mix.exs
new file mode 100644
index 0000000..6250bf6
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/mix.exs
@@ -0,0 +1,80 @@
+defmodule Arcane.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :arcane,
+ version: "0.1.0",
+ elixir: "~> 1.14",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps()
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {Arcane.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ # supabase
+ {:supabase_potion, "~> 0.5"},
+ # last fix released 24/09/2024
+ {:supabase_gotrue, "~> 0.3.10"},
+ {:supabase_storage, "~> 0.3"},
+
+ # phoenix base
+ {:phoenix, "~> 1.7.14"},
+ {:phoenix_ecto, "~> 4.5"},
+ {:ecto_sql, "~> 3.10"},
+ {:postgrex, ">= 0.0.0"},
+ {:phoenix_html, "~> 4.1"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
+ {:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
+ {:floki, ">= 0.30.0", only: :test},
+ {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
+ {:telemetry_metrics, "~> 1.0"},
+ {:telemetry_poller, "~> 1.0"},
+ {:jason, "~> 1.2"},
+ {:dns_cluster, "~> 0.1.1"},
+ {:bandit, "~> 1.5"}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ # For example, to install project dependencies and perform other setup tasks, run:
+ #
+ # $ mix setup
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
+ "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+ "ecto.reset": ["ecto.drop", "ecto.setup"],
+ test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
+ "assets.setup": ["esbuild.install --if-missing"],
+ "assets.build": ["esbuild arcane"],
+ "assets.deploy": [
+ "esbuild arcane --minify",
+ "phx.digest"
+ ]
+ ]
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/mix.lock b/examples/user_managment/phoenix_live_view_user_management/mix.lock
new file mode 100644
index 0000000..579f4b4
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/mix.lock
@@ -0,0 +1,44 @@
+%{
+ "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
+ "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
+ "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
+ "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
+ "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
+ "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"},
+ "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
+ "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
+ "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
+ "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
+ "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
+ "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
+ "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
+ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
+ "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
+ "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
+ "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
+ "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
+ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
+ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
+ "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
+ "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.6", "47d2669995ea326e5c71f5c1bc9177109cebf211385c638faa7b5862a401e516", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e56e4f1642a0b20edc2488cab30e5439595e0d8b5b259f76ef98b1c4e2e5b527"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
+ "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
+ "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"},
+ "supabase_gotrue": {:hex, :supabase_gotrue, "0.3.10", "acc7ba45199bfbe1b1283730d880d27eb1d14d9aee8f9bdec65db3fab9e695ca", [:mix], [{:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.4", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "3700e6fb859a06f6417dab76060f9046debd2c6bbcb6d57661de190183f2b05d"},
+ "supabase_potion": {:hex, :supabase_potion, "0.5.1", "3f604c875edc8895010552f6b36ba03fe5f281813234e337adb930dd2f7df178", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c26a9e99fd61fc546694c7a5ae48c4c8ab36295230eb28de04818e1b59610c23"},
+ "supabase_storage": {:hex, :supabase_storage, "0.3.4", "e6dd3f560cd330a5c0af372a629a592b1850a4ad4245f086fcdfa03364ea54b8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.4", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "884db370fcce62dcf3d128b20c27f3dcf3ea649df5aa531ec5347b3180ca3a56"},
+ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
+ "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
+ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
+}
diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs
new file mode 100644
index 0000000..49f9151
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs
@@ -0,0 +1,4 @@
+[
+ import_deps: [:ecto_sql],
+ inputs: ["*.exs"]
+]
diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs
new file mode 100644
index 0000000..4bd5582
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs
@@ -0,0 +1,18 @@
+defmodule Arcane.Repo.Migrations.CreateProfiles do
+ use Ecto.Migration
+
+ def change do
+ create table(:profiles, primary_key: false) do
+ add :id, references(:users, prefix: "auth", type: :binary_id), primary_key: true
+ add :username, :text
+ add :avatar_url, :text
+ add :website, :text
+
+ # inserted_at and updated_at
+ timestamps()
+ end
+
+ create unique_index(:profiles, :username)
+ create constraint(:profiles, :username, check: "char_length(username) >= 3")
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs
new file mode 100644
index 0000000..2be359e
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs
@@ -0,0 +1,41 @@
+defmodule Arcane.Repo.Migrations.CreateProfilesPolicies do
+ use Ecto.Migration
+
+ def up do
+ execute("alter table profiles enable row level security;")
+
+ execute("""
+ create policy "Public profiles are viewable by everyone."
+ on profiles for select
+ using ( true );
+ """)
+
+ execute("""
+ create policy "Users can insert their own profile."
+ on profiles for insert
+ with check ( (select auth.uid()) = id );
+ """)
+
+ execute("""
+ create policy "Users can update own profile."
+ on profiles for update
+ using ( (select auth.uid()) = id );
+ """)
+ end
+
+ def down do
+ execute("alter table profiles disable row level security;")
+
+ execute("""
+ drop policy "Public profiles are viewable by everyone." on profiles;
+ """)
+
+ execute("""
+ drop policy "Users can insert their own profile." on profiles;
+ """)
+
+ execute("""
+ drop policy "Users can update own profile." on profiles;
+ """)
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs
new file mode 100644
index 0000000..64beda7
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs
@@ -0,0 +1,27 @@
+defmodule Arcane.Repo.Migrations.SetUpStorage do
+ use Ecto.Migration
+
+ def up do
+ execute("""
+ create policy "Avatar images are publicly accessible."
+ on storage.objects for select
+ using ( bucket_id = 'avatars' );
+ """)
+
+ execute("""
+ create policy "Anyone can upload an avatar."
+ on storage.objects for insert
+ with check ( bucket_id = 'avatars' );
+ """)
+ end
+
+ def down do
+ execute("""
+ drop policy "Avatar images are publicly accessible." on storage.objects;
+ """)
+
+ execute("""
+ drop policy "Anyone can upload an avatar." on storage.objects;
+ """)
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs
new file mode 100644
index 0000000..95c2555
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs
@@ -0,0 +1,14 @@
+# Script for populating the database. You can run it as:
+#
+# mix run priv/repo/seeds.exs
+#
+# Inside the script, you can read and write to any of your
+# repositories directly:
+#
+# Arcane.Repo.insert!(%Arcane.SomeSchema{})
+#
+# We recommend using the bang functions (`insert!`, `update!`
+# and so on) as they will fail if something goes wrong.
+Arcane.Repo.query!("""
+insert into storage.buckets (id, name) values ('avatars', 'avatars');
+""")
diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico b/examples/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico
new file mode 100644
index 0000000..7f372bf
Binary files /dev/null and b/examples/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico differ
diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/static/robots.txt b/examples/user_managment/phoenix_live_view_user_management/priv/static/robots.txt
new file mode 100644
index 0000000..26e06b5
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore b/examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore
new file mode 100644
index 0000000..a3ad880
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore
@@ -0,0 +1,4 @@
+# Supabase
+.branches
+.temp
+.env
diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml b/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml
new file mode 100644
index 0000000..d8bce59
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml
@@ -0,0 +1,173 @@
+# A string used to distinguish different Supabase projects on the same host. Defaults to the
+# working directory name when running `supabase init`.
+project_id = "arcane"
+
+[api]
+enabled = true
+# Port to use for the API URL.
+port = 54321
+# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
+# endpoints. `public` is always included.
+schemas = ["public", "graphql_public"]
+# Extra schemas to add to the search_path of every request. `public` is always included.
+extra_search_path = ["public", "extensions"]
+# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
+# for accidental or malicious requests.
+max_rows = 1000
+
+[db]
+# Port to use for the local database URL.
+port = 54322
+# Port used by db diff command to initialize the shadow database.
+shadow_port = 54320
+# The database major version to use. This has to be the same as your remote database's. Run `SHOW
+# server_version;` on the remote database to check.
+major_version = 15
+
+[db.pooler]
+enabled = false
+# Port to use for the local connection pooler.
+port = 54329
+# Specifies when a server connection can be reused by other clients.
+# Configure one of the supported pooler modes: `transaction`, `session`.
+pool_mode = "transaction"
+# How many server connections to allow per user/database pair.
+default_pool_size = 20
+# Maximum number of client connections allowed.
+max_client_conn = 100
+
+[realtime]
+enabled = true
+# Bind realtime via either IPv4 or IPv6. (default: IPv4)
+# ip_version = "IPv6"
+# The maximum length in bytes of HTTP request headers. (default: 4096)
+# max_header_length = 4096
+
+[studio]
+enabled = true
+# Port to use for Supabase Studio.
+port = 54323
+# External URL of the API server that frontend connects to.
+api_url = "http://127.0.0.1"
+# OpenAI API Key to use for Supabase AI in the Supabase Studio.
+openai_api_key = "env(OPENAI_API_KEY)"
+
+# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
+# are monitored, and you can view the emails that would have been sent from the web interface.
+[inbucket]
+enabled = true
+# Port to use for the email testing server web interface.
+port = 54324
+# Uncomment to expose additional ports for testing user applications that send emails.
+# smtp_port = 54325
+# pop3_port = 54326
+
+[storage]
+enabled = true
+# The maximum file size allowed (e.g. "5MB", "500KB").
+file_size_limit = "50MiB"
+
+[storage.image_transformation]
+enabled = true
+
+[auth]
+enabled = true
+# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
+# in emails.
+site_url = "http://127.0.0.1:4000/session/confirm"
+# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
+additional_redirect_urls = ["https://127.0.0.1:4000"]
+# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
+jwt_expiry = 3600
+# If disabled, the refresh token will never expire.
+enable_refresh_token_rotation = true
+# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
+# Requires enable_refresh_token_rotation = true.
+refresh_token_reuse_interval = 10
+# Allow/disallow new user signups to your project.
+enable_signup = true
+# Allow/disallow anonymous sign-ins to your project.
+enable_anonymous_sign_ins = false
+# Allow/disallow testing manual linking of accounts
+enable_manual_linking = false
+
+[auth.email]
+# Allow/disallow new user signups via email to your project.
+enable_signup = true
+# If enabled, a user will be required to confirm any email change on both the old, and new email
+# addresses. If disabled, only the new email is required to confirm.
+double_confirm_changes = true
+# If enabled, users need to confirm their email address before signing in.
+enable_confirmations = false
+# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
+max_frequency = "1s"
+
+# Uncomment to customize email template
+# We need that for Phoenix since we're using Server Side Rendering
+# So we will be doing authentication on the Server
+[auth.email.template.magic_link]
+subject = "Your Magic Link"
+content_path = "./supabase/templates/magic_link.html"
+
+[auth.sms]
+# Allow/disallow new user signups via SMS to your project.
+enable_signup = true
+# If enabled, users need to confirm their phone number before signing in.
+enable_confirmations = false
+# Template for sending OTP to users
+template = "Your code is {{ .Code }} ."
+# Controls the minimum amount of time that must pass before sending another sms otp.
+max_frequency = "5s"
+
+# Use pre-defined map of phone number to OTP for testing.
+# [auth.sms.test_otp]
+# 4152127777 = "123456"
+
+# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
+# [auth.hook.custom_access_token]
+# enabled = true
+# uri = "pg-functions:////"
+
+# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
+[auth.sms.twilio]
+enabled = false
+account_sid = ""
+message_service_sid = ""
+# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
+auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
+
+# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
+# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
+# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
+[auth.external.apple]
+enabled = false
+client_id = ""
+# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
+secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
+# Overrides the default auth redirectUrl.
+redirect_uri = ""
+# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
+# or any other third-party OIDC providers.
+url = ""
+# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
+skip_nonce_check = false
+
+[analytics]
+enabled = false
+port = 54327
+vector_port = 54328
+# Configure one of the supported backends: `postgres`, `bigquery`.
+backend = "postgres"
+
+# Experimental features may be deprecated any time
+[experimental]
+# Configures Postgres storage engine to use OrioleDB (S3)
+orioledb_version = ""
+# Configures S3 bucket URL, eg. .s3-.amazonaws.com
+s3_host = "env(S3_HOST)"
+# Configures S3 bucket region, eg. us-east-1
+s3_region = "env(S3_REGION)"
+# Configures AWS_ACCESS_KEY_ID for S3 bucket
+s3_access_key = "env(S3_ACCESS_KEY)"
+# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
+s3_secret_key = "env(S3_SECRET_KEY)"
diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/seed.sql b/examples/user_managment/phoenix_live_view_user_management/supabase/seed.sql
new file mode 100644
index 0000000..e69de29
diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html b/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html
new file mode 100644
index 0000000..f17ac62
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html
@@ -0,0 +1,7 @@
+Magic Link
+
+Follow this link:
+
+
+ Log In
+
\ No newline at end of file
diff --git a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs
new file mode 100644
index 0000000..56883a8
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs
@@ -0,0 +1,14 @@
+defmodule ArcaneWeb.ErrorHTMLTest do
+ use ArcaneWeb.ConnCase, async: true
+
+ # Bring render_to_string/4 for testing custom views
+ import Phoenix.Template
+
+ test "renders 404.html" do
+ assert render_to_string(ArcaneWeb.ErrorHTML, "404", "html", []) == "Not Found"
+ end
+
+ test "renders 500.html" do
+ assert render_to_string(ArcaneWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs
new file mode 100644
index 0000000..55a99ed
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs
@@ -0,0 +1,12 @@
+defmodule ArcaneWeb.ErrorJSONTest do
+ use ArcaneWeb.ConnCase, async: true
+
+ test "renders 404" do
+ assert ArcaneWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
+ end
+
+ test "renders 500" do
+ assert ArcaneWeb.ErrorJSON.render("500.json", %{}) ==
+ %{errors: %{detail: "Internal Server Error"}}
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs
new file mode 100644
index 0000000..3a184f5
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule ArcaneWeb.PageControllerTest do
+ use ArcaneWeb.ConnCase
+
+ test "GET /", %{conn: conn} do
+ conn = get(conn, ~p"/")
+ assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex b/examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex
new file mode 100644
index 0000000..72b939a
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex
@@ -0,0 +1,38 @@
+defmodule ArcaneWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use ArcaneWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # The default endpoint for testing
+ @endpoint ArcaneWeb.Endpoint
+
+ use ArcaneWeb, :verified_routes
+
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import ArcaneWeb.ConnCase
+ end
+ end
+
+ setup tags do
+ Arcane.DataCase.setup_sandbox(tags)
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex b/examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex
new file mode 100644
index 0000000..cb96496
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex
@@ -0,0 +1,58 @@
+defmodule Arcane.DataCase do
+ @moduledoc """
+ This module defines the setup for tests requiring
+ access to the application's data layer.
+
+ You may define functions here to be used as helpers in
+ your tests.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use Arcane.DataCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ alias Arcane.Repo
+
+ import Ecto
+ import Ecto.Changeset
+ import Ecto.Query
+ import Arcane.DataCase
+ end
+ end
+
+ setup tags do
+ Arcane.DataCase.setup_sandbox(tags)
+ :ok
+ end
+
+ @doc """
+ Sets up the sandbox based on the test tags.
+ """
+ def setup_sandbox(tags) do
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Arcane.Repo, shared: not tags[:async])
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+ end
+
+ @doc """
+ A helper that transforms changeset errors into a map of messages.
+
+ assert {:error, changeset} = Accounts.create_user(%{password: "short"})
+ assert "password is too short" in errors_on(changeset).password
+ assert %{password: ["password is too short"]} = errors_on(changeset)
+
+ """
+ def errors_on(changeset) do
+ Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
+ Regex.replace(~r"%{(\w+)}", message, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
+end
diff --git a/examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs b/examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs
new file mode 100644
index 0000000..65d4d27
--- /dev/null
+++ b/examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(Arcane.Repo, :manual)
diff --git a/flake.nix b/flake.nix
index 98f6c92..aed0294 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,7 +31,7 @@
mkShell {
name = "supabase-ex";
packages = with pkgs;
- [beam.elixir_1_17]
+ [beam.elixir_1_17 postgresql]
++ lib.optional stdenv.isLinux [inotify-tools]
++ lib.optional stdenv.isDarwin [
darwin.apple_sdk.frameworks.CoreServices
diff --git a/guides/quickstarts/phoenix_liveview.md b/guides/quickstarts/phoenix_liveview.md
new file mode 100644
index 0000000..876b9a9
--- /dev/null
+++ b/guides/quickstarts/phoenix_liveview.md
@@ -0,0 +1,87 @@
+# Use Supabase with Phoenix LiveView
+
+Learn how to create a LiveView project and connect it to your Supabase Postgres database.
+
+## 1. Create a Phoenix LiveView Project
+
+Make sure your Elixir and Phoenix installer versions are up to date, then use `mix phx.new` to scaffold a new LiveView project. Postgresql is the default database for Phoenix apps.
+
+Go to the [Phoenix docs](https://phoenixframework.org) for more details.
+
+```sh
+mix phx.new blog
+```
+
+## 2. Set up the Postgres connection details
+
+Go to [database.new](https://database.new/) and create a new Supabase project. Save your database password securely.
+
+When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string. Make sure **Use connection pooling** is checked and **Session mode** is selected. Then copy the URI. Replace the password placeholder with your saved database password.
+
+> [!INFO]
+> If your network supports IPv6 connections, you can also use the direct connection string. Uncheck **Use connection pooling** and copy the new URI.
+
+For the production environment, you can set up this env var on your `config/runtime.exs`
+```sh
+export DATABASE_URL=ecto://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
+```
+
+For your local dev environment your can modify the `config/dev.exs` file to look like this (replacing placeholders with your `supabase-cli` config):
+
+```elixir
+# config/dev.exs
+
+import Config
+
+config :blog, Blog.Repo,
+ hostname: "localhost",
+ port: 54322, # default supabase-cli postgres port
+ username: "postgres",
+ password: "postgres",
+ database: "postgres"
+
+# other configs
+```
+
+## 3. Create and run a database migration
+
+Phoenix LiveView includes [Ecto](https://hexdocs.pm/ecto) as the data mapping and database schema magement tool (aka ORM in other stacks) as well as database migration tooling which generates the SQL migration files for you.
+
+Create an example `Article` model and generate the migration files.
+
+```sh
+mix phx.gen.schema Posts.Article articles title:string views:integer
+mix ecto.migrate
+```
+
+The first argument is the schema module followed by its plural name (used as the table name).
+
+The generated schema above will contain:
+- a schema file in `lib/blog/posts/article.ex`, with a articles table
+- a migration file for the repository
+
+More information on the [mix phx.new.schema task documentation](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Schema.html)
+
+## 4. Use the Model to interact with the database
+
+You can use the included Phoenix console to interact with the database. For example, you can create new entries or list all entries in a Model's table.
+
+```sh
+iex -S mix
+```
+
+```iex
+article = %Blog.Posts.Article{title: "Hello Phoenix", body: "I am on Phoenix!"}
+Blog.Repo.insert!(article) # Saves the entry to the database
+Blog.Repo.all(Blog.Posts.Article) # Lists all the entries of a model in the database
+```
+
+## 5. Start the app
+
+Run the development server. Go to [http://127.0.0.1:4000](http://127.0.0.1:4000) in a browser to see your application running.
+
+```sh
+iex -S mix phx.server
+```
+
+> This command also starts an iex session (REPL) while staring the web server
diff --git a/lib/supabase/client/behaviour.ex b/lib/supabase/client/behaviour.ex
index ecf9dba..02975c4 100644
--- a/lib/supabase/client/behaviour.ex
+++ b/lib/supabase/client/behaviour.ex
@@ -1,5 +1,5 @@
defmodule Supabase.Client.Behaviour do
- @doc """
+ @moduledoc """
The behaviour for the Supabase Client. This behaviour is used to define the API for a Supabase Client.
If you're implementing a [Self Managed Client](https://github.com/zoedsoupe/supabase-ex?tab=readme-ov-file#self-managed-clients) as the [Supabase.Client](https://hexdocs.pm/supabase_potion/Supabase.Client.html), this behaviour is already implemented for you.