{"id":15725878,"url":"https://github.com/ndrean/phoenix_solid","last_synced_at":"2025-10-08T16:24:39.493Z","repository":{"id":179744471,"uuid":"663561514","full_name":"ndrean/phoenix_solid","owner":"ndrean","description":"Phoenix runs a SolidJS SPA","archived":false,"fork":false,"pushed_at":"2023-08-16T22:43:06.000Z","size":976,"stargazers_count":16,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-13T10:48:29.708Z","etag":null,"topics":["elixir","phoenix-liveview","solidjs"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ndrean.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-07T15:25:33.000Z","updated_at":"2025-03-20T21:55:17.000Z","dependencies_parsed_at":"2024-05-04T23:35:32.778Z","dependency_job_id":null,"html_url":"https://github.com/ndrean/phoenix_solid","commit_stats":null,"previous_names":["ndrean/phoenix_solid"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ndrean/phoenix_solid","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fphoenix_solid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fphoenix_solid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fphoenix_solid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fphoenix_solid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ndrean","download_url":"https://codeload.github.com/ndrean/phoenix_solid/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fphoenix_solid/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274935501,"owners_count":25376830,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-13T02:00:10.085Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","phoenix-liveview","solidjs"],"created_at":"2024-10-03T22:24:47.543Z","updated_at":"2025-10-08T16:24:34.460Z","avatar_url":"https://github.com/ndrean.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PhxSolid\n\n\u003chttps://docs.coincap.io/#37dcec0b-1f7b-4d98-b152-0217a6798058\u003e\n\nThis project demonstrates a way to run clustered containers of a Phoenix web app with a SPA embedded, backed by a PostgreSQL database and connected to a Livebook node to monitor the web app nodes. It also describes how you can set up authenticated websockets to share information or state between the Phoenix backend and the SPA. For example, we see below some info on the backend, such as nodes events and clustered nodes. These are \"real-time\" information passed to the SPA via the socket connection.\n\n\u003cimg width=\"478\" alt=\"Screenshot 2023-07-25 at 17 03 43\" src=\"https://github.com/ndrean/phoenix_solid/assets/6793008/ad0998dd-b608-42ae-b228-ae37e508d6a4\"\u003e\n\nThe project describes recipes of how to include a [SolidJS](https://www.solidjs.com/) app in a Phoenix app in two ways:\n\n- embedded with a \"hook\" in a Liveview,\n- or rendered on a separate page from a controller with `Plug.Conn.send_resp`\n\nWhy would you do this? Many apps are developed as hybrid web apps: a SPA communicating with a backend.\n\nWhy `SolidJS`? It is used because it is lightweight, doesn't use a VDOM and is almost as fast as Vanilla Javascript when compared to say `React`.\n\nIf you don't have navigation within the SPA, it can be useful to embed the Javascript into a hook. If you have navigation within the SPA (this is the case here), then you lose your Liveview connection.\n\nWhat are the differences between the two options?\n\n- the full page is built with `Vite` (with Esbuild and Rollup). The compilation of the full-page code is a custom process, run via a `Task`. The embedded version is compiled with `Esbuild` via a modified `mix assets.deploy`: you set up a custom \"build\" version of Esbuild. Rollup is _more performant_ than Esbuild to minimize the size of the bundles.\n- to use authenticated websockets with an authenticated user, we need to [adapt the documentation](https://hexdocs.pm/phoenix/channels.html#using-token-authentication).\n\nFrom the app, you can navigate to the LiveDashboard.\n\n\u003cimg width=\"898\" alt=\"Screenshot 2023-07-25 at 17 07 49\" src=\"https://github.com/ndrean/phoenix_solid/assets/6793008/6cd70751-6586-4475-9dc4-eb5c601a6182\"\u003e\n\nYou can connect to a Livebook. You can connect to the database as the cluster shares the same Docker network. This enables you not to open the Postgres database.\n\n\u003cimg width=\"626\" alt=\"Screenshot 2023-07-25 at 17 02 24\" src=\"https://github.com/ndrean/phoenix_solid/assets/6793008/1e3b896c-c85e-42cf-abff-c612616e78de\"\u003e\n\nTo communicate with the Phoenix app, you need authenticated websocket. An authentication is proposed (Google One Tap, using a Magic link login \u003chttps://johnelmlabs.com/posts/magic-link-auth\u003e or anonymous account).\n\n\u003cdetails\u003e\u003csummary\u003eAuthenticate websockets\u003c/summary\u003e\nWe first generate a `Phoenix.Token`. When we use the embedded SPA, we pass this \"user token\" into the `conn.assigns` from a Phoenix controller and it will be available in the HTML \"root.html.heex\" template. It is hard coded, attached to the `window` object so Javascript is able to read it. For the backend Liveview, we pass it into a session so available in the `Phoenix.LiveView.mount/3` callback. The embedded version will be declared via a dataset `phx-hook` and rendered in a dedicated component. For the fullpage version, a controller will `Plug.Conn.send_resp` the compiled \"index.html\" file of the SPA. In the controller, we hard code the token (available in the \"conn.assigns\") into this file. Then Javascript will be able to read it and use it.\n\u003c/details\u003e\n\n## \"hooked\" SPA\n\n### Esbuild\n\nYou set up a custom `Esbuild` configuration to use the [custom plugin `solidPlugin`](https://github.com/amoutonbrady/esbuild-plugin-solid). Since SolidJS uses JSX for templating, we have to be sure Esbuild compiles the JSX files for **SolidJS**.\n\nThe Phoenix documentation explains [how to add a plugin](https://hexdocs.pm/phoenix/asset_management.html#esbuild-plugins). Esbuild will build the assets when we run the following function:\n\n\u003cdetails\u003e\u003csummary\u003ebuild.js\u003c/summary\u003e\n\n```js\n// build.js\nimport { context, build } from \"esbuild\";\nimport { solidPlugin } from \"esbuild-plugin-solid\";\n\nconst args = process.argv.slice(2);\nconst watch = args.includes(\"--watch\");\nconst deploy = args.includes(\"--deploy\");\n\n// Define esbuild options\nlet opts = {\n  entryPoints: [\"js/app.js\", \"js/solidAppHook.js\"],\n  bundle: true,\n  logLevel: \"info\",\n  target: \"es2021\",\n  outdir: \"../priv/static/assets\",\n  external: [\"*.css\", \"fonts/*\", \"images/*\"],\n  loader: { \".js\": \"jsx\", \".svg\": \"file\" },\n  plugins: [solidPlugin()],\n  format: \"esm\",\n};\n\nif (deploy) {\n  opts = {\n    ...opts,\n    minify: true,\n    splitting: true,\n  };\n  build(opts);\n}\n\nif (watch) {\n  opts = {\n    ...opts,\n    sourcemap: \"inline\",\n  };\n\n  context(opts)\n    .then((ctx) =\u003e {\n      ctx.watch();\n    })\n    .catch((_error) =\u003e {\n      process.exit(1);\n    });\n}\n```\n\n\u003c/details\u003e\n\nThe \"config.exs\" file will only contain the required version:\n\n```elixir\n# config.exs\nconfig :esbuild,\n  version: \"0.17.11\"\n```\n\nThe documentation explains to modify the alias `mix assets.deploy` defined in the Mix.Project: you run `node build.js --deploy` in the \"/assets\" folder.\n\n```elixir\n\"assets.deploy\": [\n  \"tailwind default --minify\",\n  \"cmd --cd assets node build.js --deploy\",\n  \"phx.digest\"\n]\n```\n\n\u003e Check how to [configure Tailwind with Phoenix](https://tailwindcss.com/docs/guides/phoenix)\n\nSince we use code splitting, you will also need to:\n\n- add \"type=module\" in the \"my_app_web/components/layouts/root.html.heex\" file as code splitting works with ESM (using `import`).\n\n```html\n\u003cscript defer phx-track-static type=\"module\" type=\"text/javascript\" src={~p\"/assets/app.js\"}\u003e\u003c/script\u003e\n```\n\n- and declare you are using `\"type\": \"module\"` in \"/assets/package.json\"\n\n```js\n//...\n\"type\": \"module\",\n\"dependencies\": {\n   \"@solidjs/router\": \"^0.8.2\",\n   \"bau-solidcss\": \"^0.1.14\",\n   \"phoenix\": \"file:../deps/phoenix\",\n   \"phoenix_html\": \"file:../deps/phoenix_html\",\n   \"phoenix_live_view\": \"file:../deps/phoenix_live_view\",\n   \"solid-js\": \"^1.7.7\",\n   \"topbar\": \"^2.0.1\"\n },\n \"devDependencies\": {\n   \"esbuild\": \"^0.18.11\",\n   \"esbuild-plugin-solid\": \"^0.5.0\",\n   \"@tailwindcss/forms\": \"^0.5.4\",\n   \"tailwindcss\": \"^3.3.3\"\n }\n```\n\n#### Mount a SPA as a hook to a LiveView\n\nWe will mount a LiveView and render the SPA inside a component. This component has a dataset `phx-hook=\"solidAppHook\"`. This hook references the SPA Javascript code.\n\n```elixir\nuse Phoenix.Component\ndef display(assigns) do\n  ~H\"\"\"\n  \u003cdiv id=\"solid\" phx-hook=\"SolidAppHook\" phx-update=\"ignore\"\u003e\u003c/div\u003e\n  \"\"\"\nend\n```\n\nWe attach to the property \"hooks\" of the `LiveSocket` (the one authenticated with the `_csrf_token`) the function that renders the SPA.\n\n```js\n//app.js\nimport { Socket } from \"phoenix\";\nimport { SolidAppHook } from \"./solidAppHook';\n\nnew LiveSocket(\"/live\", Socket, {\n  params: { _csrf_token: csrfToken },\n  hooks: { SolidAppHook }\n}).connect();\n\n```\n\nThe code of the hook looks like this:\n\n```js\n//SolidAppHook.js\nconst SolidAppHook = {\n  mounted(){import(...). then((App)=\u003e render(...)}\n}\n```\n\nYou set up a \"user_socket\" and authenticate it in the backend with the \"user token\". We will attach a `channel`to have two ways of communication between the front and the back.\n\n## Navigation with Phoenix/Liveview\n\nOnce you are authenticated via the sign-in, you are redirected to a Liveview. We set up a tab-like navigation where you can choose to navigate to the SPA in a full page or display the embedded SPA. On this page, all the code for the embedded SPA is already loaded.\n\nNote that the SPA has an internal navigation. When you use it in the embedded version, you disconnect from the LiveView. The full-page version is also disconnected from the Liveview.\n\n\u003e An `on mount` function is run on each mount of the LiveView as [recommended by the doc](https://hexdocs.pm/phoenix_live_view/security-model.html#mounting-considerations).\n\n## **non hook** SPA\n\nThe boilerplate is:\n\n```bash\ncd phx_solid\nnpx degit solidjs/templates/js front\n```\n\n### Set up\n\n- `Vite`: use `base: \"/spa\"` to pass the correct path in the build.\n\n```js\nexport default defineConfig({\n  plugins: [solidPlugin()],\n  base: \"/spa/\",\n         ^^^\n  build: {\n    target: \"esnext\",\n  },\n});\n```\n\n- modify \"/front/src/index.html\". In this file, add a \"title\" in the \"head\" tag. This will help to insert programmaticaly the \"user_token\" in this file as seen further down.\n\n```html\n\u003ctitle\u003eSolid App\u003c/title\u003e\n```\n\n- installed dependencies: install [phoenix.js](https://www.npmjs.com/package/phoenix)\n\n```js\n// /front/package.json\n// ...\n\"devDependencies\": {\n    \"solid-devtools\": \"^0.27.3\",\n    \"vite\": \"^4.3.9\",\n    \"vite-plugin-solid\": \"^2.7.0\"\n  },\n  \"dependencies\": {\n    \"@solidjs/router\": \"^0.8.2\",\n    \"bau-solidcss\": \"^0.1.15\",\n    \"phoenix\": \"^1.7.6\",\n    \"solid-js\": \"^1.7.6\"\n  }\n```\n\n- `Phoenix`: in the module \"app_web.ex\", add the folder \"spa\" to \"static_paths\" so the \"endpoint.ex\" gets the correct config through `plug Plug.Static, only: PhxSolidWeb.static_paths()`\n\n```elixir\n  def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) ++ [\"spa\"]\n```\n\n### Build the rendered SPA\n\nWe will compile the \"front\" files and copy them into the folder \"priv/static/spa\". We set up a [mix task](https://hexdocs.pm/mix/Mix.html) for this. Run this before anything.\n\n```bash\nmix spa --path=\"./priv/static/spa\"\n```\n\n### Render the \"non-hook\" SPA\n\nThe route \"/spa\" will call the controller \"spa_controller\". It reads the compiled \"index.html\" file from the \"priv/static/spa\" folder and adds the \"user_token\" inside a \"script\" tag. To put this into the \"head\" tag, we added `\u003ctitle\u003eSolid app\u003c/title\u003e` in the \"index.html\" file of the SPA. When we read the file line by line and encounter this particular line, we add the \"script\" tag\" with the \"user_token\" value from the session. We end the controller with a `Plug.Conn.send_resp`.\n\nNote that the file path is defined by the function below. We need to add `Application.app_dir(:phx_solid)` for the `mix release` task to find this file.\n\n```elixir\ndefp index_html do\n  Application.app_dir(:phx_solid) \u003c\u003e \"/\" \u003c\u003e\n  System.get_env(:phx_solid, :spa_dir)\n  \u003c\u003e  \"index.html\"\nend\n```\n\n### Return from SPA to Phoenix\n\nThe SPA offers a navigation, in particular a link to return to Phoenix. We need to pass this via env variables. This is done with `Vite` with `import.meta.env.VITE_XXX`. Vite already has `dotenv` installed as [explained by the doc](https://vitejs.dev/guide/env-and-mode.html#env-files). You can use just like this to reference the URL to which we want to navigate back.\n\n```js\n\u003ca href={import.meta.env.VITE_RETURN_URL}\u003e...\u003c/a\u003e\n```\n\n```bash\n# .env\nVITE_RETURN_URL=http://localhost:4000/welcome\n```\n\n\u003e this has to be tested when deployed for real !!!\n\n## User token\n\nWe generate a token per user after the sign-in.\n\n```elixir\nPhoenix.Token.sign(PhxSolidWeb.Endpoint,\"user_token\", id )\n```\n\nWe can check the validity of the websocket connection since we will check the token with the alter ego function `Phoenix.Token.verify`\n\n## Passing data between the SPA and Phoenix\n\nEven if the SPA is fully functional, we are just rendering HTML so when we navigate back and forth between Phoenix and the SPA, the state of the SPA is lost.\n\nIn order to save the _state of the SPA_, we use channels through the `Socket` object\n\n### The `socket`\n\nIt is an object that holds the WS. We will set up the socket SPA side and server side. We generate the 2 files - server \u0026 client - needed to handle bith sides of the socket. As previously stated, make sure the npm package `Phoenix.js` is installed in the SPA.\n\n```bash\nmix phx.gen.socket User\ncd front \u0026\u0026 pnpm i phoenix\n```\n\n#### Client-side\n\nIn the SPA's \"index.jsx\" file (where we `render`), we instantiate the socket connection with the `Socket` object and pass along the `user_token` read from the DOM. It will be available in the query string of the \"ws\", hence params, and is received server-side to authenticate and thus permit the connection.\n\n```js\n// userSocket.js\nimport { Socket } from \"phoenix\";\n\nconst socket = new Socket(\"/socket\", {\n  params: { token: window.userToken },\n});\n\nif (window.userToken) socket.connect();\n\nexport default socket;\n```\n\nWe also built a helper `useChannel`. It attaches a channel to the socket with a topic and returns the channel, ready to be used (`.on`, `.push`). Use it every time you need to create a channel and communicate with the backend. It has a cleaning stage in its life cycle. For example, the SPA has navigation; when we use a page, it opens a channel for the data on this page, and when we leave this page, this channel is closed.\n\n```js\nimport { onCleanup } from \"solid-js\";\n\nexport default function useChannel(socket, topic) {\n  if (!socket) return null;\n  const channel = socket.channel(topic, { user_token: window.userToken });\n  channel\n    .join()\n    .receive(\"ok\", () =\u003e {\n      console.log(\"Joined successfully\");\n    })\n    .receive(\"error\", (resp) =\u003e {\n      console.log(\"Unable to join\", resp);\n    });\n  onCleanup(() =\u003e {\n    console.log(\"closing channel\");\n    channel.leave();\n  });\n\n  return channel;\n}\n```\n\n#### Server-side\n\nWe add to our \"endpoint.ex\":\n\n```elixir\n# endpoint.ex\nsocket \"/socket\", PhxSolidWeb.UserSocket,\n  websocket: true,\n  longpoll: false\n```\n\nServer-side, the \"user_socket.ex\" module is invoked and receives the \"user_token\" in the params. We verify it:\n\n```elixir\nPhoenix.Token.verify(PhxSolidWeb.Endpoint, \"user token\", token, max_age: 86_400)\n```\n\nWe used `App.Endpoint` since `conn` is not available.\n\nThe connection should be fine now.\n\n### Channels\n\nA channel is an Elixir process derived from a Genserver: it is therefore capable of emitting and receiving messages. It is uniquely identified by a string and attached to the `socket` which accepts a list of channels. This is done in the _UserSocket_ module.\n\nWhenever we `push` data through a channel client-side, its alter ego server-side will receive it in a callback `handle_in`.\nWe can push data from the server to the client through the socket with a `broadcast!(topic, event, message)` or `push` related to a topic. The client will receive it with the listener `channel_topic.on(event, (resp)=\u003e{...})`.\n\nTo set up a channel, use the generator:\n\n```bash\nmix phx.gen.channel Counter\n```\n\nWe create channels per piece of UI state we want to save. For example, we count the number of times the SPA landing page is reached. We save this counter as a **singleton table** (one row). Th\n\n## Docker\n\n### Dockerfile\n\nIt is a 3 stages process with Debian 11 based images:\n\n- a builder stage for the full page SPA based on a NodeJS 18 Debian 11 based image. In dev non-docker mode, you can build \"by hand\" `mix spa --path=\"./priv/static/spa\"`. This stage is used to differenciate the rebuild from the hooked version.\n- a builder stage for the Phoenix app and its JS assets, based on Elixir with NodeJS injected, and produce a release and compiled JS assets. We inject the full page SPA here.\n- the final \"runner\" stage to deliver a minimal Debian-based image.\n\n\u003e We need to install `nodejs` and `npm`, then `pnpm` as (curiously???) NPM didn't accept \"link:../deps/phoenix..\".\n\n### Docker-Compose and Postgres init\n\nWe run 4 services: 2 instances of the web app, the Postgres database and a Livebook.\n\nTo start a Postgres container, it is enough to pass the env variables `POSTGRES_PASSWORD`, `POSTGRES_USER` and `POSTGRES_DB`. This will create a database.\n\nThe web app uses a `DATABASE_URL` env variable in the form below. Note that the \"hostname\" is the **service name\\*** (and not \"localhost\" as in dev non-docker mode)\n\n```elixir\necto://\u003cuser\u003e:\u003cpass\u003e@\u003cservice\u003e/\u003cPOSTGRES_DB_{MIX_ENV}\u003e\n```\n\nTo run the migrations, we will use the Docker entrypoint \"docker-entrypoint-initdb.d\" and bind the `init.sql` file from the host into this directory of the Postgres container.\n\nTo generate this file, we use the [code generated by the migration in DEV mode](https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Migrate.html#module-command-line-options):\n\n```elixir\nmix ecto.migrate --log-migrations-sql \u003e ./init.sql\n```\n\nIt will remain to clean this file to play it.\n\n\u003cdetails\u003e\n\u003csummary\u003e--- The docker-compose file ---\u003c/summary\u003e\n\n```bash\nversion: \"3.9\"\n\nvolumes:\n  pg-data:\n\nnetworks:\n  mynet:\n\nx-web-app: \u0026commun-web-app\n  image: phx_solid\n  depends_on:\n    - db\n  environment:\n    RELEASE_DISTRIBUTION: sname\n  env_file:\n    - .env-docker\n  networks:\n    - mynet\n\nservices:\n  db:\n    image: postgres:15.3-bullseye\n    env_file:\n      - .env-docker\n    restart: always\n    networks:\n      - mynet\n    volumes:\n      - pg-data:/var/lib/postgresql/data\n      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro\n    ports:\n      - \"5432\"\n\n  livebook:\n    image: ghcr.io/livebook-dev/livebook\n    networks:\n      - mynet\n    depends_on:\n      - db\n    environment:\n      - MIX_ENV=prod\n      - LIVEBOOK_DISTRIBUTION=sname\n      - LIVEBOOK_COOKIE=supersecret\n      - LIVEBOOK_PASSWORD=securesecret\n      - SECRET_KEY_BASE=HRPM+KVxrXtYiIni27wn1pXrNc/cl7wjHl/u5TWQxqZkuvJ6Q4NBF+WMUVUpQVIY\n    hostname: livebook\n    volumes:\n      - ./data:/data/\n    ports:\n      - \"8080:8080\"\n      - \"8081:8081\"\n\n  app0:\n    \u003c\u003c: *commun-web-app\n    hostname: app0\n    ports: - \"4000:4000\"\n\n  app1:\n    \u003c\u003c: *commun-web-app\n    hostname: app1\n    ports: - \"4001:4000\"\n```\n\n\u003c/details\u003e\n\nTo build this, run:\n\n```bash\ndocker build -t phx_solid .\ndocker-compose up\n```\n\nIn the Livebook container, we will bind a local folder to the \"/data\" folder to save the \".livemd\" file that contains the markdown we want to run in the Livebook.\n\nYou may use `Base.url_encode64(:crypto.strong_rand_bytes(40))` to generate the env variable `RELEASE_COOKIE`.\n\n### Livebook node discovery\n\nTo enable node discovery, add the `libcluster` dependency and the same code as in the web app:\n\n```elixir\ntopologies = [gossip: [strategy: Cluster.Strategy.Gossip]]\n\nchildren = [\n  {Cluster.Supervisor, [topologies, [name: Lv.ClusterSupervisor]]}\n]\n\nopts = [strategy: :one_for_one, name: PhxSolid.Supervisor]\nSupervisor.start_link(children, opts)\n```\n\nSince the Livebook node is hidden, you need to set up the node monitoring as below if you want to capture a `:nodeup` (or down) event:\n\n```elixir\n:net_kernel.monitor_nodes(true, %{node_type: :all})\n```\n\nYou can check:\n\n```elixir\nNode.list(:connected)\n```\n\n```elixir\n:rpc.call(:\"phx_solid@app0\", PhxSolid.Repo, :get_by, [PhxSolid.SocialUser, %{id: 1}])\n```\n\n## State persistence\n\nWith \"standard\" SSR, the backend manages the state, and the UI is a simple rendering machine\nThe SPA itself can use state management. Since it is lost each time you disconnect, it may need to be persisted. We used a \"context\" pattern in the SPA.\nWe could set up a Redis session or use the database. If the app is distributed, most probably Redis or the database should be used.\n\n## Misc\n\n### Add Google One Tap\n\nTo enable **Google One tap**, there is a module `:google_certs`. It needs the dependencies\n\n```elixir\n{:jason, \"~\u003e 1.4\"},{:joken, \"~\u003e 2.5\"}\n```\n\n`Joken` will bring in `JOSE` which is used to decrypt the PEM version and JWK version.\n\nYou will need credentials from Google.\n\n- create a project in the API library: \u003chttps://console.cloud.google.com/apis/library\u003e\n- then create or select a projecct, and go for the credentials as a **web application**\n- ⚠️ the \"Authorized Javascript origins\" should contain **2** fields, **one with AND another without the port**.\n\nGet the HTML with Google's [code generator](https://developers.google.com/identity/gsi/web/tools/configurator).\n\nYou set up a \"one_tap_controller\". It is a POST endpoint and will receive a response from Google. It will set a `user_token` and the users' `profile` in the session, and redirect to a \"welcome\" page.\n\n\u003cimg width=\"502\" alt=\"Screenshot 2023-07-07 at 16 51 37\" src=\"https://github.com/ndrean/phoenix_solid/assets/6793008/b07428c8-1722-49f9-9003-6f9b513eb1e4\"\u003e\n\n#### Source .env\n\nDon't forget to add the credentials in \".env\".\n\n```bash\n# .env-dev\nexport GOOGLE_CLIENT_ID=xxx\nexport GOOGLE_CLIENT_SECRET=xxx\n```\n\nand source them:\n\n```bash\nsource .env-dev\n```\n\n### Content Security Policy\n\nIn the `router` module, you will set the CSP as per [Google's recommendations](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#content_security_policy)\n\n```elixir\nplug(\n  :put_secure_browser_headers,\n  %{\"content-security-policy-report-only\" =\u003e @csp}\n)\n```\n\n```elixir\n@csp \"\nscript-src https://accounts.google.com/gsi/client;\nframe-src https://accounts.google.com/gsi/;\nconnect-src https://accounts.google.com/gsi/;\n\"\n```\n\nYou will also need to secure the scripts used to pass the token to the `window` object. This can be done with a `nonce`.\n\n### Serving static files\n\nWe could further reduce the load on the Phoenix backend by using a reverse proxy (Nginx \u003e Caddy) with cache control. It would serve the static files and pass the WS connections and HTTP connections to the backend.\n\n#### Nginx\n\nThe easiest way to use Nginx is to use a container running an NGINX image. We can mount the config file and the static files inside it.\n\n\u003e Relative paths in Nginx are resolved based on the Nginx installation directory, not the current working directory or the location of the configuration file.\n\u003e It will serve the static files and reverse proxy the app.\n\nCreate a Dockerfile that takes an NGINX image and copy the static files \"priv/static/assets\" and \"/priv/static/spa\" into the folder \"/usr/share/nginx/\".\n\n```bash\ndocker build -t webserver -f ./docker/nginx/Dockerfile .\ndocker run -it --rm -p 80:80 --name web -v $(pwd)/solid.conf:/etc/nginx/conf.d/default.conf webserver\n```\n\nThe image will use the underlying `entrypoint` and `cmd` provided by the NGINX image. Enter in it and check:\n\n```bash\ndocker exec -it web bash\nls /usr/share/nginx/\n```\n\n### Notes on SQLITE\n\nGist: \u003chttps://gist.github.com/mcrumm/98059439c673be7e0484589162a54a01\u003e\n\nLitestream: \u003chttps://litestream.io/\u003e. Stream the db.\n\n[Migration in a release without Mix installed](https://hexdocs.pm/phoenix/releases.html#ecto-migrations-and-custom-commands): \"release.ex\"\n\nIn \"application.ex\", do:\n\n```elixir\n PhxSolid.Release.migrate()\n```\n\n[Upserts with SQLite3](https://www.sqlite.org/lang_UPSERT.html) works when the target field has a unique constraint (`create unique_index` in the migration):\n\n```elixir\nRepo.insert!(\n  %User{email: email, name: name, logs: 1},\n  conflict_target: [:email],\n  on_conflict: [\n    inc: [logs: 1],\n    set: [updated_at: DateTime.utc_now()]\n  ]\n)\n```\n\n[Sqlite3 CLI](https://www.sqlite.org/cli.html) (dot notation):\n\n```bash\n~/phx_solid/db\u003e .open phx_solid.db\nsqlite\u003e .mode tabs\nsqlite\u003e select * from social_users;\nsqlite .quit\n```\n\n### CSS Typewriter\n\nTypewriter effect: \u003chttps://dev.to/lazysock/make-a-typewriter-effect-with-tailwindcss-in-5-minutes-dc\u003e\n\nConfiguration in Tailwind.config\n\n### TypedEctoSchema\n\n\u003chttps://hexdocs.pm/typed_ecto_schema/TypedEctoSchema.html?ref=blixt-dev\u003e\n\n### Kaffy\n\nTo be checked: \u003chttps://github.com/aesmail/kaffy?ref=blixt-dev\u003e\n\n### Caddy\n\nUse `Caddy server` to reverse-proxy Cowboy. The Facebook login will work. Just do:\n\n```bash\ncaddy reverse-proxy --from :80 --to: 4000\n# or if you use a config file:\ncaddy run Caddyfile\n```\n\nAlternatively, you can use:\n\n```bash\nmix phx.gen.cert\n```\n\nand modify your \"config.exs\":\n\n```elixir\nconfig :phx_solid, PhxSolidWeb.Endpoint,\n  https: [\n  port: 4001,\n  cipher_suite: :strong,\n  certfile: \"priv/cert/selfsigned.pem\",\n  keyfile: \"priv/cert/selfsigned_key.pem\"\n]\n```\n\nWith Chrome, set up \"enable\" on `chrome://flags/#allow-insecure-localhost`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fphoenix_solid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fndrean%2Fphoenix_solid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fphoenix_solid/lists"}