<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: aaronblondeau</title>
    <description>The latest articles on DEV Community by aaronblondeau (@aaronblondeau).</description>
    <link>https://dev.to/aaronblondeau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F812978%2F602e93c5-a69a-4790-aa67-3fcc6ecd593f.png</url>
      <title>DEV Community: aaronblondeau</title>
      <link>https://dev.to/aaronblondeau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aaronblondeau"/>
    <language>en</language>
    <item>
      <title>Me and Claudie Poo</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Tue, 10 Mar 2026 00:57:57 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/me-and-claudie-poo-2dol</link>
      <guid>https://dev.to/aaronblondeau/me-and-claudie-poo-2dol</guid>
      <description>&lt;p&gt;About twenty seconds into trying out &lt;a href="https://claude.com/product/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; I realized that there is no going back on this AI adventure we're all on. My mission as a software developer has always been to be as capable as possible. Now that mission includes my new buddy "Claudie Poo". Going forward, WE need to be as capable as possible together.&lt;/p&gt;

&lt;p&gt;That means several things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I can't get stupid by letting Claudie do everything for me, that would let us both down.&lt;/li&gt;
&lt;li&gt;My role is going to shift upwards towards oversight.&lt;/li&gt;
&lt;li&gt;Choosing what to work on is going to be more important than ever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;One : The human has to know what they're doing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I create the folder structure. I choose which packages we're going to use. I always write the first code. By writing the first class/controller/component in each new unit of code I establish patterns for everything else that AI generates. By sticking to this practice I stay hands on with the code and keep my skills sharp. I also know where everything is.&lt;/p&gt;

&lt;p&gt;I am also sticking to my skills development schedule. I like to learn a new language every year. I also try to take a course or read a book on TypeScript every year. The Vue.js docs are an annual must-read too. One of the keys to innovation is knowing what is possible. By continuing to be a code craftsman I will stay well prepared to lead both humans and AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two : Use tools that bring harmony&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since that first moment I started using Claude Code I have been adjusting which tools I use in my projects. These 3 have been critical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://orpc.dev/" rel="noopener noreferrer"&gt;oRPC&lt;/a&gt; &lt;a href="https://orpc.dev/docs/contract-first/define-contract" rel="noopener noreferrer"&gt;(with contract first development)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vitest.dev/" rel="noopener noreferrer"&gt;Vitest&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;oRPC has revolutionized how I work. Seamlessly using TypeScript in both frontend and backend is just so insanely efficient. A neat side effect of oRPC is that it moves collaboration between humans and AI towards a more organized codebase. I specifically use oRPC's contract first development capabilities. I the human write a code based contract of what the app should do. Then it is extremely clear for the AI what should be done.&lt;/p&gt;

&lt;p&gt;Zod works great with oRPC and helps humans and AI agree on what shape the data in an app should take. As I write each oRPC contract for an app backend I use zod schemas to be very specific about how I want the data to look. Then ol' Claudie Poo can clearly read my intent and create well structured database tables, routers, and so on.&lt;/p&gt;

&lt;p&gt;Automated tests always used to take too long to create and maintain. Not anymore! Vitest has been the perfect tool for me to communicate what the final output of the app should be like. By writing automated tests together, Claudie and I can make sure that we're shipping a slop free product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three : Code is cheap but time still isn't&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I recently had Claudie Poo create a custom WordPress plugin for me so that I didn't have to pay a monthly subscription for a feature I needed.  You can build your next startup idea in days only to have someone copy it in hours. Code is no longer an asset. Traction is.&lt;/p&gt;

&lt;p&gt;That leaves me at a total loss for my next side project idea. Because I have always focused on the code first I have wound up building the wrong thing over and over. Now, I have no choice but to strike a different path. I just hope I don't wander too long before I find it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>No Code For You (AI)</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Wed, 31 Dec 2025 19:18:24 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/no-code-for-you-ai-5emb</link>
      <guid>https://dev.to/aaronblondeau/no-code-for-you-ai-5emb</guid>
      <description>&lt;p&gt;Why do I use GitHub so much? I am literally handing my code over to an AI obsessed mega corporation! That is an extremely stupid thing to do. Why do I do this? Because I'm lazy and everyone else is doing it.&lt;/p&gt;

&lt;p&gt;Is there a way I can still be lazy and ensure that I have full autonomy over my code?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes&lt;/strong&gt; - with &lt;a href="https://about.gitea.com/products/gitea/" rel="noopener noreferrer"&gt;Gitea&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;I am not going to opine too much on the importance of code autonomy here, but the more I think about how I've handed all my personal code over to Microsoft over the years the sicker I feel. I get a wrong feeling about multi billion dollar mega corps slurping up people's code for free and then turning around and charging them to use models trained on their code.&lt;/p&gt;

&lt;p&gt;I feel a little bit better knowing that AI will probably read this post and then help people to setup their own git hosting thus reducing the amount of free code it can gobble up. Wait, the LLM training process can probably read that last sentence there and won't include tokens from this post. Dang it!&lt;/p&gt;

&lt;p&gt;Anyways, there are a couple of really easy ways to get started with Gitea such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.gitea.com/gitea-cloud/" rel="noopener noreferrer"&gt;Gitea Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caprover.com/docs/one-click-apps" rel="noopener noreferrer"&gt;Caprover&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://elest.io/open-source/gitea" rel="noopener noreferrer"&gt;elestio&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For myself, I decided to host Gitea on my own VM (running Ubuntu). What follows are the steps I used to set it all up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker setup
&lt;/h2&gt;

&lt;p&gt;Installing docker on a fresh VM is really easy : &lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;https://docs.docker.com/engine/install/ubuntu/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Caddy setup (reverse proxy)
&lt;/h2&gt;

&lt;p&gt;I also installed caddy directly on the VM : &lt;a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian" rel="noopener noreferrer"&gt;https://caddyserver.com/docs/install#debian-ubuntu-raspbian&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gitea setup with docker-compose
&lt;/h2&gt;

&lt;p&gt;Gitea needs a database to store information. It can use a couple of different options : &lt;a href="https://docs.gitea.com/installation/database-prep" rel="noopener noreferrer"&gt;https://docs.gitea.com/installation/database-prep&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I chose postgres. To run it I used a docker-compose.yml file that lives in it's own directory and looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5432:5432&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/db:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./initdb.d:/docker-entrypoint-initdb.d:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the same directory as this docker-compose.yaml file I have an .env file with a root user and password for postgres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I launched postgres with this command (run in same directory as the docker-compose file)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the postgres container was running I created new databases and users with this process:&lt;/p&gt;

&lt;p&gt;1) Get the id of the container running postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Shell into the container with docker exec&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; CONTAINER_ID_GOES_HERE bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Login to postgres. Make sure you're in the postgres container when running this. It will ask for the postgres password. You set this password in the .env file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-h&lt;/span&gt; localhost &lt;span class="nt"&gt;-p&lt;/span&gt; 5432 &lt;span class="nt"&gt;-U&lt;/span&gt; myuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Run SQL commands to setup a user and a database&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;gitea&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="n"&gt;gitea&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;encrypted&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="s1"&gt;'supersecretpasswordgoeshere'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;privileges&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="n"&gt;gitea&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;gitea&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5) Logout of postgres&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\q
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6) Logout of container&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gitea setup with docker-compose
&lt;/h2&gt;

&lt;p&gt;For Gitea I also deployed the following docker-compose.yml file (which also lives in it's own directory on the host):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea/gitea:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gitea:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# I already had something else running on 3000 so I mapped it to 3030 on the host&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3030:3000"&lt;/span&gt;
      &lt;span class="c1"&gt;# Use a different SSH port than host&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2222:22"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I also ran this docker-compose file with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup DNS and Caddy
&lt;/h2&gt;

&lt;p&gt;Before setting up Caddy to point to Gitea, I setup a DNS A record that points to the domain name I wanted to use for Gitea.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gitea.mydomain.com A my.ip.goes.here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like to setup DNS first when using Caddy so it doesn't have problems &lt;a href="https://caddyserver.com/docs/automatic-https" rel="noopener noreferrer"&gt;setting up https&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then I added an entry to my Caddy config to proxy requests for "gitea.mydomain.com" to port 3030&lt;/p&gt;

&lt;p&gt;To do this I edited /etc/caddy/Caddyfile by adding a reverse_proxy entry to the end of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gitea.mydomain.com {
    reverse_proxy localhost:3030
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next I reloaded the caddy config so it will pickup the changes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl reload caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup backup
&lt;/h2&gt;

&lt;p&gt;I made sure that the VM I am using gets backed up nightly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Gitea
&lt;/h2&gt;

&lt;p&gt;Once Caddy had reloaded and setup https I was able to access it at gitea.mydomain.com.&lt;/p&gt;

&lt;p&gt;The first time you visit a Gitea instance it will guide you through setting up the database configuration as well as a user login.&lt;/p&gt;

&lt;p&gt;I used the database name, user name, and password I setup in postgres for gitea.&lt;/p&gt;

&lt;p&gt;The only interesting config option I made was I had to use 172.17.0.1:5432 for the postgres host. 172.17.0.1 is sort of like the "localhost" for docker containers on linux. On mac and windows you can use host.docker.internal:5432 for the host.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fce7jchgnrk4w7iviwtoj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fce7jchgnrk4w7iviwtoj.jpg" alt="Database configuration for Gitea" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Gitea
&lt;/h2&gt;

&lt;p&gt;Gitea will be very familiar to GitHub users. You can create repos, clone, branch, commit, push, create PR's and so on. The only change I had to make was to make a small tweak to the ssh config on my laptop so that I could access repos via ssh.&lt;/p&gt;

&lt;p&gt;I added the following to my ~/.ssh/config file to make sure git used port 2222 when accessing my gitea instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# My Self Hosted Gitea
Host gitea.mydomain.com
    Port 2222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>git</category>
      <category>github</category>
      <category>gitea</category>
    </item>
    <item>
      <title>Who wants to build an admin UI? Part 2 : Custom Field Editors</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Sat, 08 Nov 2025 18:25:34 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/who-wants-to-build-an-admin-ui-part-2-custom-field-editors-4eob</link>
      <guid>https://dev.to/aaronblondeau/who-wants-to-build-an-admin-ui-part-2-custom-field-editors-4eob</guid>
      <description>&lt;p&gt;In my last post I detailed how &lt;a href="https://kottster.app/" rel="noopener noreferrer"&gt;Kottster&lt;/a&gt; offers a great way to create an admin UI for startup projects. In that post I lamented how Kottster didn't yet offer a way to provide a custom editor UI for specific database columns. Turns out they do and I had missed it in their docs. To test out this feature I created two non-trivial field editor components.&lt;/p&gt;

&lt;p&gt;I created a location picker with a clickable map that uses GeoJSON:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej3deo23q2712vaxduzn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej3deo23q2712vaxduzn.png" alt="Screenshot of a location picker component in Kottster" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also created a file uploader which sends files to S3:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0bjqs2ql6zfqk0u6878.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0bjqs2ql6zfqk0u6878.png" alt="Screenshot of a photo uploader component in Kottster" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of my code is here : &lt;a href="https://github.com/aaronblondeau/bigfoot-sightings" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/bigfoot-sightings&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here are the steps I followed to setup my development environment:
&lt;/h2&gt;

&lt;p&gt;1) I created a new Kottster project (Typescript and pnpm): &lt;a href="https://kottster.app/docs/" rel="noopener noreferrer"&gt;https://kottster.app/docs/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2) I installed drizzle (SQLite)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm add drizzle-orm @libsql/client dotenv
pnpm add -D drizzle-kit tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) I created a .env file with the path to my SQLite db.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_FILE_NAME=file:bigfoot-sightings.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Following the drizzle config steps I created a schema.ts file: &lt;a href="https://github.com/aaronblondeau/bigfoot-sightings/blob/main/schema.ts" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/bigfoot-sightings/blob/main/schema.ts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This schema has users and sightings (this is a demo app for bigfoot sightings)&lt;/p&gt;

&lt;p&gt;5) I created drizzle.config.ts : &lt;a href="https://github.com/aaronblondeau/bigfoot-sightings/blob/main/drizzle.config.ts" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/bigfoot-sightings/blob/main/drizzle.config.ts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;6) I ran drizzle push to create the database and schema that I would use with Kottster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx drizzle-kit push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7) I launched Kottster and created my admin login.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8) Next I connected my Kottster instance to my SQLite database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7u82vao9l7baizk8o4jj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7u82vao9l7baizk8o4jj.png" alt="Screenshot of setting up database connection" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;9) I added a page for the users table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4q145321i5r4qohwubxw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4q145321i5r4qohwubxw.png" alt="Screenshot of creating users table" width="492" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;10) And a page for the sightings table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furwoqc4gnx0itexujfvw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furwoqc4gnx0itexujfvw.png" alt="Screenshot of creating sightings table" width="490" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;11) After creating these tables you can see that the default TextField editors would make it difficult to edit the photo and location columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw62zcf6l2btd2qhgq15b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw62zcf6l2btd2qhgq15b.png" alt="Screenshot of default row editors" width="440" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;12) The pages created in steps 9 and 10 above generate code in the app/pages folder of the Kottster instance. Check out the GitHub repo for all the code I created to make these custom field editor happen. &lt;/p&gt;

&lt;h2&gt;
  
  
  Discoveries
&lt;/h2&gt;

&lt;p&gt;Overall the process of creating a custom field plugin was easy and straightforward. The docs for creating custom field components are here : &lt;a href="https://kottster.app/docs/table/customization/custom-fields#modify-field-input-for-existing-columns" rel="noopener noreferrer"&gt;https://kottster.app/docs/table/customization/custom-fields#modify-field-input-for-existing-columns&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;1) Move your renderComponent to its own file to prevent browser reloads.&lt;/p&gt;

&lt;p&gt;Whenever you edit a file in the app/pages folder, Kottster will restart and reload the browser window. This makes developing a custom component pretty frustrating.&lt;/p&gt;

&lt;p&gt;By moving my editor components to their own file I was able to get back into the usual React hot reload development cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TablePage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@kottster/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;LocationEditor&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../../components/LocationEditor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FileUploader&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../../components/FileUploader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TablePage&lt;/span&gt;
    &lt;span class="nx"&gt;columnOverrides&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
      &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fieldInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;renderComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LocationEditor&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Photo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fieldInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;renderComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FileUploader&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Don't forget about nested tables.&lt;/p&gt;

&lt;p&gt;Kottster automatically recognizes relationships in the database and lets you edit child rows in a modal window. To provide your custom field editor in the modal window you need to add it to the nested property of your parent table. This is how I setup the sighting (child) field editors in the user (parent) page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TablePage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@kottster/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;LocationEditor&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../../components/LocationEditor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FileUploader&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../../components/FileUploader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TablePage&lt;/span&gt;
    &lt;span class="nx"&gt;nested&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
      &lt;span class="c1"&gt;// To find this key, click open a nested table in the UI.&lt;/span&gt;
      &lt;span class="c1"&gt;// The key will appear in a small badge at the top of the screen, change __c__ to __p__&lt;/span&gt;
      &lt;span class="na"&gt;sightings__p__user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;columnOverrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;fieldInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;renderComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LocationEditor&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="na"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Photo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;fieldInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;renderComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FileUploader&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first I was confused on how to find the key to use for the nested object, but later realized it was right there in the UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhns4850rebnyzcczybk4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhns4850rebnyzcczybk4.png" alt="Screenshot of nested key" width="258" height="68"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3) Beware of extra React renders&lt;/p&gt;

&lt;p&gt;I noticed a bit of inconsistency in how props are provided to the custom component depending on whether the field was for a new row or an existing row. I had to do a little bit of juggling to setup state for my component : &lt;a href="https://github.com/aaronblondeau/bigfoot-sightings/blob/main/components/LocationEditor.tsx#L22" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/bigfoot-sightings/blob/main/components/LocationEditor.tsx#L22&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;4) Use presigned file upload urls&lt;/p&gt;

&lt;p&gt;I am not sure what Kottster is using for communication between the frontend and backend for procedure calls. I do know that I quickly ran into issues trying to handle file uploads. &lt;a href="https://github.com/aaronblondeau/bigfoot-sightings/blob/main/components/FileUploader.tsx#L45" rel="noopener noreferrer"&gt;See my notes in the FileUploader component.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get around the procedure invocation limitations I wound up using a presigned upload URL to send files straight from the browser to S3. The getFileUploadUrl procedure generates the URL and then the FileUploader component uses fetch to send it straight to the bucket.&lt;/p&gt;

&lt;p&gt;5) Backend procedures need to be in both the parent and nested pages to work.&lt;/p&gt;

&lt;p&gt;I wound up creating the exact same api.server.ts file in both the users and sightings table's folders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../_server/app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileUploadProcedures&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../../lib/fileUploadProcedures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// https://kottster.app/docs/table/configuration/api#custom-server-api&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineTableController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="nx"&gt;fileUploadProcedures&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The Procedures type can be used on the frontend&lt;/span&gt;
&lt;span class="c1"&gt;// to get type-safety when calling server procedures.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Procedures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;procedures&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that the procedures are available in modal dialogs when editing child rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you need to create a quick admin UI for non-technical users to manage your app's data, give Kottster a try. It'll will get you really far out of the box and it provides customization options that handle the uses cases for most small teams and startups.&lt;/p&gt;

</description>
      <category>kottster</category>
      <category>developer</category>
      <category>typescript</category>
      <category>database</category>
    </item>
    <item>
      <title>Who wants to build an admin UI?</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Tue, 07 Oct 2025 21:48:27 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/who-wants-to-build-an-admin-ui-55b9</link>
      <guid>https://dev.to/aaronblondeau/who-wants-to-build-an-admin-ui-55b9</guid>
      <description>&lt;p&gt;&lt;a href="https://pocketbase.io/" rel="noopener noreferrer"&gt;PocketBase&lt;/a&gt; is my favorite tool for rapidly prototyping app ideas. One of the main things that I love about it is the extremely capable admin UI it comes with. Aside from asking non-technical users to edit JSON columns you can get really far with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Having an admin UI for free saves enormous amounts of development time and lets me focus on other more MVP critical areas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I discovered &lt;a href="https://kottster.app/" rel="noopener noreferrer"&gt;Kottster&lt;/a&gt; a few weeks ago and decided to try it on one of my non-PocketBase projects. However, I abandoned it pretty quickly because at the time you had to login with their hosted authentication service and there was no way to completely self host it. They must have heard my loud sigh of exasperation because they recently announced full-self hosting support : (&lt;a href="https://kottster.app/blog/kottster-is-now-fully-self-hosted)%5Bhttps://kottster.app/blog/kottster-is-now-fully-self-hosted%5D" rel="noopener noreferrer"&gt;https://kottster.app/blog/kottster-is-now-fully-self-hosted)[https://kottster.app/blog/kottster-is-now-fully-self-hosted]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Huge props to the Kottster team for recognizing that as a roadblock to adoption and addressing it!&lt;/p&gt;

&lt;p&gt;Like the PocketBase admin UI, Kottster gets you really far out of the box. You can view and edit the tables in your database and you can also create dashboards for displaying query results.&lt;/p&gt;

&lt;p&gt;Kottster operates in a neat way where as you create dashboards or table views in your instance it creates the code for them within your project's directory. This allows you to easily customize them as well as keep them in a code repo.&lt;/p&gt;

&lt;p&gt;Kottster also allows you to create custom admin pages where you are set loose with a react component for the frontend page content, and a server side controller for the backend.&lt;/p&gt;

&lt;p&gt;Here is what some of the server side code looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/_server/app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/*
 * Custom server procedures for your page
 * 
 * These functions run on the server and can be called from your React components
 * using callProcedure('procedureName', input)
 * 
 * Learn more: https://kottster.app/docs/custom-pages/api
 */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineCustomController&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// Define your procedures here&lt;/span&gt;
  &lt;span class="c1"&gt;// For example:&lt;/span&gt;
  &lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;getSetting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// .env files work just fine&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WIDGET_SETTING&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;getWidgets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// I had to reverse engineer things to create this code,&lt;/span&gt;
    &lt;span class="c1"&gt;// but direct access to the db via knex is available!&lt;/span&gt;

    &lt;span class="c1"&gt;// Another cool note is that the input and output types of&lt;/span&gt;
    &lt;span class="c1"&gt;// procedures are available in the frontend react component!&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataSources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;widgets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;widgets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
      &lt;span class="c1"&gt;// A json col...&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
      &lt;span class="p"&gt;}[]&lt;/span&gt;
      &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
      &lt;span class="na"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
    &lt;span class="p"&gt;}[],&lt;/span&gt; &lt;span class="na"&gt;totalWidgets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;totalResult&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Procedures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;procedures&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling a backend procedure from the frontend React component is really easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widgets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;callProcedure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getWidgets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://dev.to/aaronblondeau/who-wants-to-build-an-admin-ui-part-2-custom-field-editors-4eob"&gt;See my next post for info on how to create custom field editors for Kottster! &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd highly recommend you give Kottster a try. It seems like a fairly young project but is on a good trajectory and solves a problem that I think a lot of devs have.&lt;/p&gt;

</description>
      <category>kottster</category>
      <category>developer</category>
      <category>typescript</category>
      <category>database</category>
    </item>
    <item>
      <title>Self hosted maps for (practically) free</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Wed, 04 Jun 2025 00:46:04 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/self-hosted-maps-for-practically-free-1i3n</link>
      <guid>https://dev.to/aaronblondeau/self-hosted-maps-for-practically-free-1i3n</guid>
      <description>&lt;p&gt;Using &lt;a href="https://www.openstreetmap.org/" rel="noopener noreferrer"&gt;OpenStreetMap&lt;/a&gt;, &lt;a href="https://protomaps.com/" rel="noopener noreferrer"&gt;ProtoMaps&lt;/a&gt;, &lt;a href="https://maputnik.github.io/" rel="noopener noreferrer"&gt;Maputnik&lt;/a&gt; and &lt;a href="https://maplibre.org/" rel="noopener noreferrer"&gt;MapLibre&lt;/a&gt; to self host custom maps is a really fun tech adventure! These low cost and serverless maps work on the web, react native, android, and iOS. Break free from Google Maps, Apple Maps, and Mapbox!&lt;/p&gt;

&lt;p&gt;Note that the &lt;a href="https://protomaps.com/api" rel="noopener noreferrer"&gt;ProtoMaps API&lt;/a&gt; is a great way to get started with custom maps without having to do all the technical stuff below!&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;With OpenStreetMap as its foundation, the open source mapping community has some truly amazing projects. &lt;a href="https://protomaps.com/" rel="noopener noreferrer"&gt;Protomaps&lt;/a&gt; is a project that elevates the entire open mapping community.&lt;/p&gt;

&lt;p&gt;Traditionally map data has been broken down into small "tiles" of either pre-rendered image data or raw map data. These small tiles of data are then downloaded one at a time and stitched together into a map on your web browser or mobile app. It has always been a difficult task to either store map data in a database or pre-process data into enormous numbers of pre-cut map tiles. The Protomaps team solves this problem by storing map tiles in one large file using a format called &lt;a href="https://docs.protomaps.com/pmtiles/" rel="noopener noreferrer"&gt;PMTiles&lt;/a&gt;. Clever use of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests" rel="noopener noreferrer"&gt;HTTP range requests&lt;/a&gt; allows clients to download individual map tiles from this one file source. Farewell expensive &lt;a href="https://osm2pgsql.org/" rel="noopener noreferrer"&gt;Posgres&lt;/a&gt; instances!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 : Get map data from geofabrik
&lt;/h2&gt;

&lt;p&gt;Use Geofabrik's OpenStreetMap data extracts site to choose and download a region (.osm.pbf) of the world that you wish to map : &lt;a href="https://download.geofabrik.de/" rel="noopener noreferrer"&gt;https://download.geofabrik.de/&lt;/a&gt;. I recommend you choose an extract that is less than 500MB to get started with.  I live in Colorado so you'll see &lt;strong&gt;colorado-latest.osm.pbf&lt;/strong&gt; referenced in the code and commands below.&lt;/p&gt;

&lt;p&gt;Important Note : the process described in this post will work to host maps that span the entire world.  If you do this, make sure you understand the costs associated with uploading and distributing the data via your hosting platform of choice. See more info about hosting .pmtiles files here : &lt;a href="https://docs.protomaps.com/pmtiles/cloud-storage" rel="noopener noreferrer"&gt;https://docs.protomaps.com/pmtiles/cloud-storage&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 : Install Tilemaker
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tilemaker.org/" rel="noopener noreferrer"&gt;Tilemaker&lt;/a&gt; is tool for transforming map data. We can use it to take our source .osm.pbf file and transform it into a .pmtiles file. The best thing about Tilemaker is that you can use a &lt;a href="https://www.lua.org/" rel="noopener noreferrer"&gt;lua&lt;/a&gt; script to organize map features into layers however you want. In this post I am going to take trail data (&lt;a href="https://wiki.openstreetmap.org/wiki/Tag:highway%3Dpath" rel="noopener noreferrer"&gt;osm tag highway=path&lt;/a&gt;) and make it available for much higher zoom levels than it is normally available. I want my map to show trails when zoomed really far out.&lt;/p&gt;

&lt;p&gt;Get Tilemaker installed on your system : &lt;a href="https://github.com/systemed/tilemaker/tree/master?tab=readme-ov-file#installing" rel="noopener noreferrer"&gt;https://github.com/systemed/tilemaker/tree/master?tab=readme-ov-file#installing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can also use the docker image if you don't want to install directly or you're on Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 : Customize .json and .lua config files for tilemaker
&lt;/h2&gt;

&lt;p&gt;To process the OpenStreetMap data you are going to need two config files.  A .json file that describes the layers in your map, and a .lua file that processes map data and packs it into the layers.  The /resources directory in the Tilemaker repo is where I got started : &lt;a href="https://github.com/systemed/tilemaker/tree/master/resources" rel="noopener noreferrer"&gt;https://github.com/systemed/tilemaker/tree/master/resources&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since Tilemaker has great docs I am not going to dive into all the details here, but I basically took the config-example.json file and the process-example.lua file and built them up a bit with some extra logic. Note that if you use the "openmaptiles" examples you need to run the get-coastline.sh, and get-landcover.sh scripts (in the root of the repo) to get some additional map data.&lt;/p&gt;

&lt;p&gt;My .json file can be found here : &lt;a href="https://gist.github.com/aaronblondeau/0327534340e2a04c1acb4a53df00faa6" rel="noopener noreferrer"&gt;https://gist.github.com/aaronblondeau/0327534340e2a04c1acb4a53df00faa6&lt;/a&gt;&lt;br&gt;
And my .lua file can be found here: &lt;a href="https://gist.github.com/aaronblondeau/20efcaffb0d799c0f191de68952d609c" rel="noopener noreferrer"&gt;https://gist.github.com/aaronblondeau/20efcaffb0d799c0f191de68952d609c&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4 : Run tilemaker
&lt;/h2&gt;

&lt;p&gt;Once the config files are ready you can run Tilemaker with a command like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tilemaker --input ./data/colorado-latest.osm.pbf \
          --output ./data/colorado-latest.pmtiles \
          --config ./config-demo.json \
          --process ./process-demo.lua
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of Colorado processes in under a minute on my machine, so iterating on the .json and .lua files was quick and easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 : Preview data
&lt;/h2&gt;

&lt;p&gt;Once you have a .pmtiles file you can inspect it at &lt;a href="https://pmtiles.io/" rel="noopener noreferrer"&gt;https://pmtiles.io/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A couple of notes : make sure to check the background box so you can pan/zoom to find your map's region. Data may not appear right away if you're using a large .pmtiles file, so give it a minute to load.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5k1viqnjnpo8lwzvlg8w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5k1viqnjnpo8lwzvlg8w.png" alt="Screenshot of previewing map data at pmtiles.io" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 : Upload .pmtiles to S3
&lt;/h2&gt;

&lt;p&gt;Once your .pmtiles file is ready to go the best way to use it to is to upload it to a cloud storage provider that supports HTTP range requests. AWS S3 is what I used. If your .pmtiles file is large, make sure you understand the costs associated with uploading and hosting it!&lt;/p&gt;

&lt;p&gt;If you use S3, you'll need to at a minimum:&lt;/p&gt;

&lt;p&gt;1) Allow public read access to your .pmtiles file&lt;br&gt;
2) Set a CORS policy that allows web clients to read the file.&lt;/p&gt;

&lt;p&gt;Here is the CORS policy I used.  More info &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [],
        "MaxAgeSeconds": 3000
    }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Protomaps folks have instructions for more production worthy hosting setups here : &lt;a href="https://docs.protomaps.com/deploy/aws" rel="noopener noreferrer"&gt;https://docs.protomaps.com/deploy/aws&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7 : Develop map style with maputnik
&lt;/h2&gt;

&lt;p&gt;Once your .pmtiles file is available online, head over to &lt;a href="https://maputnik.github.io/" rel="noopener noreferrer"&gt;Maputnik&lt;/a&gt; to start developing your map style.&lt;/p&gt;

&lt;p&gt;Hit the "Editor button" and then start by setting up a source:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fus1vl4ivs51mxz7lz96j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fus1vl4ivs51mxz7lz96j.png" alt="Screenshot of using pmtiles file url as a Vector (PMTiles) map source in maputnik." width="512" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The add layers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdol3u8nlbfh7etxqgy3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdol3u8nlbfh7etxqgy3.png" alt="Screenshot of using pmtiles file url as a Vector (PMTiles) map source in maputnik." width="393" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not seeing anything? &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The maputnik editor may not be centered on your map region.  Use the Search box to center on a city in your map's region.&lt;/li&gt;
&lt;li&gt;Make sure the map zoom is set to something appropriate (like 12)&lt;/li&gt;
&lt;li&gt;Be sure to set a color for your layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Styling with Maputnik deserves it's own book so I won't go into all the details here, but you can get pretty far by clicking around and experimenting with layers.&lt;/p&gt;

&lt;p&gt;A big unlock for me was to make sure I used existing fonts when trying to add trail names&lt;/p&gt;

&lt;p&gt;You'll note that the default "glyphs" that maputnik uses is here. This is a way of pre-packaging all the fonts used by the style.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  "glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, when adding a symbol layer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9q4r7txrvdtauax2f755.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9q4r7txrvdtauax2f755.png" alt="Screenshot of using a symbol layer to add trail names to the map." width="393" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sure to use a font included in the glyphs' .pbf file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzm6n2mrrlvfy88ev9ja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzm6n2mrrlvfy88ev9ja.png" alt="Screenshot of using Open Sans font in the layer configuration." width="762" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also used the fantastic &lt;a href="https://americanamap.org/" rel="noopener noreferrer"&gt;Americana map style&lt;/a&gt; for example code while styling some components of my map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8 : Upload map style to S3
&lt;/h2&gt;

&lt;p&gt;Once your map style is ready, export it to JSON via the Save button in Maputnik.&lt;/p&gt;

&lt;p&gt;You'll need to double check a few things with the .json before you can start using it to show maps in your apps.&lt;/p&gt;

&lt;p&gt;1) Make sure you have a source for fonts set&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Make sure you have an image sprite set. To prevent errors on Android, do this even if you're not using a sprite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"sprite": "https://americanamap.org/sprites/sprite",
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Make sure the map has a default center and zoom and that the source layer is publicly accessible (with CORS):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"center": [-106, 38.5],
"zoom": 12,
"sources": {
"demo": {
    "type": "vector",
    "url": "pmtiles://https://somebucket.s3.us-west-2.amazonaws.com/colorado-latest.pmtiles"
  }
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the map style.json file is ready, put it online too. I hosted mine in the same bucket as my .pmtiles file and made sure it was publicly accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9 : Use with maplibregl
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://maplibre.org/" rel="noopener noreferrer"&gt;MapLibre&lt;/a&gt; ties everything together and provides a way to render your map on the web and in mobile apps.&lt;/p&gt;

&lt;p&gt;Here are code example code snippets for various platforms:&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset="UTF-8"&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&amp;gt;
    &amp;lt;title&amp;gt;Demo Map&amp;lt;/title&amp;gt;
    &amp;lt;script src="https://unpkg.com/maplibre-gl@^5.5.0/dist/maplibre-gl.js"&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link href="https://unpkg.com/maplibre-gl@^5.5.0/dist/maplibre-gl.css" rel="stylesheet" /&amp;gt;
    &amp;lt;script src="https://unpkg.com/pmtiles@3.2.0/dist/pmtiles.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id="map" style="width: 100%; height: 100vh"&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;
        const protocol = new pmtiles.Protocol();
        maplibregl.addProtocol('pmtiles', protocol.tile);
        var map = new maplibregl.Map({
            container: 'map',
            style: 'https://somebucket.s3.us-west-2.amazonaws.com/demo_pmtiles.json',
            center: [-106.0, 38.5],
            zoom: 12
        });
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  React Native
&lt;/h3&gt;

&lt;p&gt;Follow instructions for the React Native plugin here : &lt;a href="https://maplibre.org/maplibre-react-native/docs/setup/getting-started" rel="noopener noreferrer"&gt;https://maplibre.org/maplibre-react-native/docs/setup/getting-started&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MapView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@maplibre/maplibre-react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MapView&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alignSelf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stretch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;mapStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://somebucket.s3.us-west-2.amazonaws.com/demo_pmtiles.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Camera&lt;/span&gt; &lt;span class="na"&gt;centerCoordinate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;106.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;38.5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;zoomLevel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS (SwiftUI)
&lt;/h3&gt;

&lt;p&gt;Follow instructions here : &lt;a href="https://maplibre.org/maplibre-native/ios/latest/documentation/maplibre-native-for-ios/gettingstarted#SwiftUI" rel="noopener noreferrer"&gt;https://maplibre.org/maplibre-native/ios/latest/documentation/maplibre-native-for-ios/gettingstarted#SwiftUI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then setup a map view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;SimpleMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIViewRepresentable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;MLNMapView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mapView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MLNMapView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;mapView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;styleURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://somebucket.s3.us-west-2.amazonaws.com/demo_pmtiles.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
        &lt;span class="n"&gt;mapView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;38.5&lt;/span&gt;
        &lt;span class="n"&gt;mapView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;106.0&lt;/span&gt;
        &lt;span class="n"&gt;mapView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zoomLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mapView&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updateUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MLNMapView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;SimpleMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edgesIgnoringSafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#Preview {&lt;/span&gt;
    &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Android (Jetpack Compose)
&lt;/h3&gt;

&lt;p&gt;First add to build.gradle.kts (app)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;implementation("org.maplibre.gl:android-sdk:11.8.5")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.demo&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.os.Bundle&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.activity.ComponentActivity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.activity.compose.setContent&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.activity.enableEdgeToEdge&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.background&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.fillMaxSize&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.padding&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.material3.Scaffold&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.Composable&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.Modifier&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.graphics.Color&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.graphics.RectangleShape&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.tooling.preview.Preview&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.viewinterop.AndroidView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.maplibre.android.MapLibre&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.maplibre.android.annotations.MarkerOptions&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.maplibre.android.camera.CameraPosition&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.maplibre.android.geometry.LatLng&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.maplibre.android.maps.MapView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.example.demo.ui.theme.DemoMapAndroidTheme&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ComponentActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;enableEdgeToEdge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;setContent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;DemoMapAndroidTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;innerPadding&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="nc"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerPadding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO - consider using key here to prevent recomposition&lt;/span&gt;
    &lt;span class="nc"&gt;AndroidView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RectangleShape&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;mapView&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;styleUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://somebucket.s3.us-west-2.amazonaws.com/demo_pmtiles.json"&lt;/span&gt;
            &lt;span class="n"&gt;mapView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;mapView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMapAsync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="c1"&gt;// Set the style after mapView was loaded&lt;/span&gt;
                &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uiSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributionMargins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="c1"&gt;// Set the map view center&lt;/span&gt;
                    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cameraPosition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CameraPosition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LatLng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;38.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;106.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nc"&gt;MarkerOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LatLng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;38.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;106.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Major Lines Man"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSnippet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"38.5 and -106.0 cross here!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;mapView&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Preview&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MapViewPreview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>opensource</category>
      <category>openstreetmap</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>PocketBase + React Native</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Fri, 09 May 2025 16:57:19 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/pocketbase-react-native-3c82</link>
      <guid>https://dev.to/aaronblondeau/pocketbase-react-native-3c82</guid>
      <description>&lt;p&gt;I have a bit of an obsession with finding the fastest way to launch apps. My goal is to be able to create fully functional MVP's and proofs of concept in less than a day. That means being able to spin up a backend and then implement a frontend as efficiently as possible. For the backend, &lt;a href="https://pocketbase.io/" rel="noopener noreferrer"&gt;PocketBase&lt;/a&gt; has been my favorite lately. On the frontend I am still trying to find a winner. I like &lt;a href="https://quasar.dev/" rel="noopener noreferrer"&gt;Quasar&lt;/a&gt; (VueJS + Capacitor) which is fantastic for web apps, but falls a bit short for mobile apps. I've been eyeing React Native lately especially since Expo offers a ton of great plugins like &lt;a href="https://docs.expo.dev/versions/latest/sdk/location/" rel="noopener noreferrer"&gt;Location&lt;/a&gt; and also supports &lt;a href="https://docs.expo.dev/versions/latest/sdk/updates/" rel="noopener noreferrer"&gt;remote updates&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately I fell well short of being able to create a basic app in less than a day with these two. After clearing a few roadblocks, however, I think you'll be able to move much quicker than I did on my first attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broken File Attachments
&lt;/h2&gt;

&lt;p&gt;Why is there always one critical thing that just doesn't work? For a PocketBase and React Native combo, that one thing is file attachments. The PocketBase SDK docs have some good examples of how you're supposed to be able to attach files to records : &lt;a href="https://pocketbase.io/docs/files-handling/" rel="noopener noreferrer"&gt;https://pocketbase.io/docs/files-handling/&lt;/a&gt;. This however does not work on React Native on mobile devices (it works fine on web).&lt;/p&gt;

&lt;p&gt;The issue has something to do with the way that React Native handles multipart form data on mobile devices. There is also possibly an issue with the way that PocketBase handles the attachments. When using the SDK with React Native, PocketBase will save the record, but then fail to process the attachment. This leaves you with an error on the client side as well as an incomplete record in the database. Double fail.&lt;/p&gt;

&lt;p&gt;I felt really smart when I decided to try and bypass the PocketBase SDK and use the REST API via fetch instead, but that didn't work either! Whatever ReactNative does with the networking stack breaks things for fetch as well. So, here is how I wound up getting file attachments to work with an image provided by Expo's &lt;a href="https://docs.expo.dev/versions/latest/sdk/imagepicker/" rel="noopener noreferrer"&gt;ImagePicker&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// iOS doesn't provide fileName&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Parse filename from image.uri&lt;/span&gt;
  &lt;span class="nx"&gt;fileName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Prepare a FormData with the record's fields&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pocketBaseUrl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/collections/reports/records&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Use custom formatted object for file attachments&lt;/span&gt;
&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;photos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Do NOT do this here : .replace("file://", ""),&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Post the record to PocketBase's REST API with fetch&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On web I can just do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// image object provided by ImagePicker has a .file on web&lt;/span&gt;
  &lt;span class="na"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick on mobile is to set these three fields in the record sent to formData&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uri : Use whatever uri is provided by ImagePicker here - do not replace file://&lt;/li&gt;
&lt;li&gt;type : Use the mime type provided by ImagePicker&lt;/li&gt;
&lt;li&gt;name : Derive a name from either the uri (iOS) or the fileName (Android) provided by ImagePicker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There must be some part of the networking stack in React Native that sees these fields and then loads the referenced file into the HTTP post request. Where to find that documentation is a mystery, but there are quite a few StackOverflows that cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broken Realtime
&lt;/h2&gt;

&lt;p&gt;I also had to make a small tweak using the &lt;a href="https://www.npmjs.com/package/react-native-sse" rel="noopener noreferrer"&gt;react-native-sse&lt;/a&gt; module to get realtime updates working on mobile. Here is how I use it to init the PocketBase SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PocketBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncAuthStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pocketbase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;EventSource&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-sse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// SSE Polyfill : https://github.com/pocketbase/pocketbase/discussions/4893&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;EventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pocketBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://path_to_my_pocketbase_instance_running_in_firebase_studio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AsyncAuthStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pb_auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serialized&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pb_auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pb_auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PocketBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pocketBaseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Having waded through these two issues I now have a tech stack that allows for extremely fast app prototyping. However, I do not plan on using this in production anytime soon. Encountering issues like these are warnings that even larger show stoppers are still out there waiting to upend your project.&lt;/p&gt;

</description>
      <category>pocketbase</category>
      <category>reactnative</category>
      <category>typescript</category>
      <category>fix</category>
    </item>
    <item>
      <title>PocketBase + Firebase Studio</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Fri, 09 May 2025 16:43:33 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/pocketbase-firebase-studio-3gom</link>
      <guid>https://dev.to/aaronblondeau/pocketbase-firebase-studio-3gom</guid>
      <description>&lt;p&gt;I needed an excuse to try out &lt;a href="https://firebase.studio/" rel="noopener noreferrer"&gt;Firebase Studio&lt;/a&gt; so I decided to try and develop a PocketBase backend on it. It didn't work right out of the box, but it was fairly easy to get going.&lt;/p&gt;

&lt;p&gt;First, create a Go project in Firebase Studio.  Use "API server" as the environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vedbvuc93fk6icufa2v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vedbvuc93fk6icufa2v.jpg" alt="Screenshot showing selection of environment as " width="599" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the time of writing this, the default version of Go in the environment is 1.21, but PocketBase requires 1.23.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flatae416mix1bkut69lw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flatae416mix1bkut69lw.jpg" alt="Screenshot showing output of " width="395" height="43"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get a newer version of Go, open up .idx/dev.nix and update the channel to "stable-24.11":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;channel = "stable-24.11";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I love that they are using &lt;a href="https://nixos.org/" rel="noopener noreferrer"&gt;NixOS&lt;/a&gt; for these environments as it will make them very customizable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You should be prompted to rebuild the environment once you save this change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fik7wvhh9nqugyaj64zoj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fik7wvhh9nqugyaj64zoj.jpg" alt="Screenshot of " width="184" height="59"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the environment restarts, make sure you have Go 1.23+.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fget5xsviblcxi7a6aqxw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fget5xsviblcxi7a6aqxw.jpg" alt="Screenshot showing output of " width="376" height="47"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your environment will have been setup to run your app via &lt;a href="https://github.com/air-verse/air" rel="noopener noreferrer"&gt;Air&lt;/a&gt;. You will need to customize Air in order to run PocketBase.&lt;/p&gt;

&lt;p&gt;Create an air.toml file by running this command in a terminal window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;air init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the air.toml file and set args_bin to "serve"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;args_bin = ["serve"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to the terminal window that is running air.  It will be titled "[onStart] run-server"  Use ctrl+c to stop it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2d6jqw03jpod0rln3ht3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2d6jqw03jpod0rln3ht3.jpg" alt="Screenshot showing the terminal window that is running Air" width="303" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the server has stopped, replace the contents of main.go with the code found here : &lt;a href="https://pocketbase.io/docs/go-overview/" rel="noopener noreferrer"&gt;https://pocketbase.io/docs/go-overview/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then in a terminal window run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go mod tidy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the terminal window that was running air, hit the up key followed by enter to re-run the air command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3pj4xdvuno4rv7prvxf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3pj4xdvuno4rv7prvxf.jpg" alt="Screenshot showing air re-starting and building PocketBase via main.go" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll notice that the output provides the PocketBase superuser setup url. This url, however, uses &lt;a href="http://127.0.0.1:8090" rel="noopener noreferrer"&gt;http://127.0.0.1:8090&lt;/a&gt; as the host name which you won't have access to.&lt;/p&gt;

&lt;p&gt;You'll need to replace the &lt;a href="http://127.0.0.1:8090" rel="noopener noreferrer"&gt;http://127.0.0.1:8090&lt;/a&gt; with your project's public URL. To get this url, first click on the Firebase Studio icon in the left navigation bar. Then expand the "Backend Ports" section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgdp1blzckgt5zxchxtk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgdp1blzckgt5zxchxtk.jpg" alt="Screenshot showing backend ports section. Lock indicates that port 8090 is not yet public." width="287" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the lock next to port 8090 to make it publicly accessible.&lt;/p&gt;

&lt;p&gt;Then open or copy the link from one of the buttons under "actions".&lt;/p&gt;

&lt;p&gt;After constructing the admin setup url it will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://8090-firebase-pocketbasedemo-1746757209594.cluster-rhptpnrfenhe4qarq36djxjqmg.cloudworkstations.dev/_/#/pbinstal/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTc0Njc1OTc0MywiaWQiOiI0MHpjOTU4MHgwbTZjcDIiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.f7KtQqQ9nVza39RyKi_-QjARkAIq00eooNH3sEW4iak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the superuser setup url, complete the setup and you'll be up and running with PocketBase!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8u08i6tj25ki2li56vq8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8u08i6tj25ki2li56vq8.jpg" alt="Screenshot of the PocketBase superuser setup screen." width="456" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note 1 : Your PocketBase admin URL will be whatever you get from the backend ports section followed by a "/_/".  For my environment this was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://8090-firebase-pocketbasedemo-1746757209594.cluster-rhptpnrfenhe4qarq36djxjqmg.cloudworkstations.dev/_/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note 2 : To run PocketBase manually (with air stopped), you'll use the following command. This helps ensure you're using the same pb_data folder that the server uses when running with air:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go run . serve --dir ./tmp/pb_data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>pocketbase</category>
      <category>webdev</category>
      <category>backend</category>
      <category>go</category>
    </item>
    <item>
      <title>How to create custom WebView based React Native components</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Tue, 15 Apr 2025 01:05:16 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/how-to-create-custom-webview-based-react-native-components-1084</link>
      <guid>https://dev.to/aaronblondeau/how-to-create-custom-webview-based-react-native-components-1084</guid>
      <description>&lt;p&gt;I am currently working on porting a capacitor app to React Native (Expo). One of the components I need to re-create is a signature area. Unfortunately the existing components for this are pretty terrible so I decided to create my own. It turned out to be so fairly easy and I think having my own component will ultimately save me time.&lt;/p&gt;

&lt;p&gt;The code is here : &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/react-native-signature-pad&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple Usage
&lt;/h2&gt;

&lt;p&gt;First install signature_pad from npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm add signature_pad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add expo-filesystem and expo-asset to your project so that HTML can be loaded for the webview:&lt;/p&gt;

&lt;p&gt;expo-filesystem : &lt;a href="https://docs.expo.dev/versions/latest/sdk/filesystem/" rel="noopener noreferrer"&gt;https://docs.expo.dev/versions/latest/sdk/filesystem/&lt;/a&gt;&lt;br&gt;
expo-asset : &lt;a href="https://docs.expo.dev/versions/latest/sdk/asset/" rel="noopener noreferrer"&gt;https://docs.expo.dev/versions/latest/sdk/asset/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/assets/signature_pad.html" rel="noopener noreferrer"&gt;assets/signature_pad.html&lt;/a&gt; to the assets folder of your project.&lt;/p&gt;

&lt;p&gt;Copy &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/components/MobileSignaturePad.tsx" rel="noopener noreferrer"&gt;components/MobileSignaturePad.tsx&lt;/a&gt; to the components folder of your project.&lt;/p&gt;

&lt;p&gt;Copy &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/components/WebSignaturePad.tsx" rel="noopener noreferrer"&gt;components/WebSignaturePad.tsx&lt;/a&gt; to the components folder of your project.&lt;/p&gt;

&lt;p&gt;Copy &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/components/WebSignaturePad.tsx" rel="noopener noreferrer"&gt;components/SignaturePad.tsx&lt;/a&gt; to the components folder of your project.&lt;/p&gt;

&lt;p&gt;Finally, use the SignaturePad component in your project.  See &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/app/index.tsx" rel="noopener noreferrer"&gt;app/index.tsx&lt;/a&gt; for example usage.&lt;/p&gt;
&lt;h2&gt;
  
  
  Customizable Usage
&lt;/h2&gt;

&lt;p&gt;Follow the steps above. Then copy the &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/tree/main/webview/react-signature-pad" rel="noopener noreferrer"&gt;webview/react-native-signature-pad&lt;/a&gt; folder into your project.&lt;/p&gt;

&lt;p&gt;Inside of the webview/react-signature-pad folder run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also run the webview code with vite&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To update the html used in the react native project (/assets/signature_pad.html) run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;The project is setup to provide support for Android, iOS and Web. When used on iOS and Android a &lt;a href="https://dev.toreact-native-webview"&gt;WebView&lt;/a&gt; is used to render a single file HTML page containing a web app that implements the signature pad. When the React Native code is used on the web, the React component that implements the signature pad is used directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgn1lkc8p4waxa1by3zo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgn1lkc8p4waxa1by3zo.png" alt="Diagram showing relationship between components" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the web component
&lt;/h2&gt;

&lt;p&gt;Here is how I created the custom signature pad component:&lt;/p&gt;

&lt;p&gt;First I added a "webview" folder to the top level of my react native project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir webview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I started a react (regular react, not react-native) project with vite inside of the webview folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd webview
npm create vite@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose "react-signature-pad" as the name, React as the framework, and TypeScript as the variant.&lt;/p&gt;

&lt;p&gt;Why vite? My end goal was for the web content to be a single html file (via &lt;a href="https://www.npmjs.com/package/vite-plugin-singlefile" rel="noopener noreferrer"&gt;vite-plugin-singlefile&lt;/a&gt;) that is easy to embed in the react native application.&lt;/p&gt;

&lt;p&gt;After doing some cleanup I implemented the component as a standalone web app using &lt;a href="https://www.npmjs.com/package/signature_pad" rel="noopener noreferrer"&gt;signature_pad&lt;/a&gt; from npm.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/webview/react-signature-pad/src/App.tsx" rel="noopener noreferrer"&gt;App.tsx&lt;/a&gt; file contains all the code to send data and events back and forth between the web application and the react native application. It does this by adding some methods to the global namespace so that they can be called via React Native webview's &lt;a href="https://github.com/react-native-webview/react-native-webview/blob/master/docs/Guide.md#communicating-between-js-and-native" rel="noopener noreferrer"&gt;injectJavaScript&lt;/a&gt;. Events and data are then sent back up to the webview by calling window.ReactNativeWebView.postMessage().&lt;/p&gt;

&lt;p&gt;Note that you cannot return values from injectJavaScript calls. So if you need to get data from inside of the webview you need to call a method that then postMessage's the data you need. In order to get the PNG image data for the signature, the code first uses injectJavaScript to call a method called "emitSignatureData".  emitSignatureData then calls signature_pad's toDataURL to get the data and sends it back with postMessage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fle03rvwqimseswu93kqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fle03rvwqimseswu93kqg.png" alt="Diagram showing data flow between react native and web content in a webview component" width="575" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/components/WebSignaturePad.tsx" rel="noopener noreferrer"&gt;WebSignaturePad.tsx&lt;/a&gt; does the work of creating a canvas and setting up signature_pad. WebSignaturePad provides a few events via props and some methods via &lt;a href="https://react.dev/reference/react/forwardRef" rel="noopener noreferrer"&gt;forwardRef&lt;/a&gt; and &lt;a href="https://react.dev/reference/react/useImperativeHandle" rel="noopener noreferrer"&gt;useImperativeHandle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Quick react rant...  What in the hell is useImperativeHandle supposed to mean? Whoever is in charge of naming things badly on the react team needs to stop being a smartass and/or go find something else to work on. I avoided using react for years because I thought "useEffect" was so stupid. Rant complete...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Note that I used the deprecated version of forwardRef because Expo isn't fully up to date with React 19 yet.&lt;/p&gt;

&lt;p&gt;Also note that I wound up moving the WebSignaturePad.tsx component to the main react-native project in order to resolve some TypeScript errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packaging the web component
&lt;/h2&gt;

&lt;p&gt;There are some limitations when it comes to using webviews. The main one being that it cannot load local html files. If your content is super simple you can just use an html string. If your content is complex the webview component docs recommend you implement a &lt;a href="https://github.com/react-native-webview/react-native-webview/blob/1ddfe70521725c365cf8accf2a1bdf82eb4db80f/docs/Guide.md#loading-local-html-files" rel="noopener noreferrer"&gt;local webserver&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since using a local webserver seems like a bad idea. I decided to leverage vite to package the entire web app into a single file. &lt;/p&gt;

&lt;p&gt;First I setup vite-plugin-singlefile&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install vite-plugin-singlefile --save-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used instructions from the module's npm page, but used "react()"" instead of "vue()"&lt;/p&gt;

&lt;p&gt;I also tweaked vite.config.ts to make output go to ../assets/signature_pad.html instead of dist/index.html&lt;/p&gt;

&lt;p&gt;My final vite.config.ts is &lt;a href="https://github.com/aaronblondeau/react-native-signature-pad/blob/main/webview/react-signature-pad/vite.config.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This setup is really nice because I can run npm build in the webview source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd webview/react-signature-pad
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deploys the single signature_pad.html file needed for the webview.&lt;/p&gt;

&lt;p&gt;Note that I didn't have to do anything special to get react native to load the .html file into a string for use with the webview.  There are some instructions &lt;a href="https://dev.to/somidad/read-text-asset-file-in-expo-356a"&gt;here&lt;/a&gt; that may be helpful if it doesn't work out of the box for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Should I move this into an npm package or is it more useful as just example code?&lt;/p&gt;

</description>
      <category>react</category>
      <category>reactnative</category>
      <category>typescript</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Hack your DNS for better focus</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Sun, 02 Mar 2025 03:24:11 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/hack-your-dns-for-better-focus-2ghd</link>
      <guid>https://dev.to/aaronblondeau/hack-your-dns-for-better-focus-2ghd</guid>
      <description>&lt;p&gt;Keeping on top of what is flowing in and out of my house on the internet has been critical for my kids' mental health. Ultimately they will need to be able to moderate their own behavior online. In the meantime it is my job to make sure that the dopamine dealers stay out.&lt;/p&gt;

&lt;p&gt;Same goes for me too. My dopamine dealer is YouTube. Every time I run into a difficult problem at work my brain says, "Hmmm, that looks hard, let's take a quick YouTube break." Then two hours later I find that difficult problem still waiting for me.&lt;/p&gt;

&lt;p&gt;I've used &lt;a href="https://nextdns.io/" rel="noopener noreferrer"&gt;NextDNS&lt;/a&gt; for years to keep our home internet safe for the kids and it works really well. Fortunately NextDNS offers API access that you can use to automate turning different internet filters on and off. Unfortunately their API docs are horrible.&lt;/p&gt;

&lt;p&gt;Here is how I setup a tool that automatically blocks YouTube during working hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 : Get an API key.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To find your NextDNS API key head to your profile page and look in the API section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 : Add YouTube to the "Websites, Apps &amp;amp; Games" section of the Parental Control tab.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1rpoicppbmaziv712ku2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1rpoicppbmaziv712ku2.png" alt="Image description" width="800" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 : Forget about the &lt;a href="https://nextdns.github.io/api/" rel="noopener noreferrer"&gt;NextDNS API docs&lt;/a&gt;. Use a browser inspector to figure out how to turn off or on the filter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I couldn't make much sense of the NextDNS API docs and almost gave up on this one, but then I decided to see if I could reverse engineer the calls from their own web UI. I opened up the inspector, cleared out the network traffic and then toggled the YouTube filter on and off. Sure enough the URL and payload to turn the filter off and on was easy to pick out:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk31r9ced3z947wsop3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk31r9ced3z947wsop3d.png" alt="Image description" width="670" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 : Use Deno to create a simple script and cron&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using the information you gather from the browser inspector, create fetch calls that will make the same request. The fetch should use your API key in a X-Api-Key header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateYouTube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.nextdns.io/profiles/YOUR-PROFILE-ID/parentalControl/services/hex:FIND-ME-IN-INSPECTOR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Api-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NEXTDNS_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Good tool to help debug timezone day/time differences:&lt;/span&gt;
&lt;span class="c1"&gt;// https://savvytime.com/converter/utc-to-co-denver/jan-13-2025/3-30pm&lt;/span&gt;

&lt;span class="c1"&gt;// Switch to standard at 6pm M-F&lt;/span&gt;
&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set standard profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3 1 * * 2-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~~ setting standard profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateYouTube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Switch to workday at 9am M-F&lt;/span&gt;
&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set workday profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3 16 * * 2-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~~ setting workday profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateYouTube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that it may take some brain wattage to get the cron times correct due to timezone issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 : Host on &lt;a href="https://deno.com/deploy" rel="noopener noreferrer"&gt;Deno Deploy&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The folks at Deno have made hosting scripts like this extremely easy. The Deno.cron statements above work without any extra config! I just put my code into a GitHub repo and linked it to my account to deploy : &lt;a href="https://docs.deno.com/deploy/manual/how-to-deploy/" rel="noopener noreferrer"&gt;https://docs.deno.com/deploy/manual/how-to-deploy/&lt;/a&gt;. Don't forget to setup the NEXTDNS_API_KEY environment variable in the service's settings.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pexels.com/@alscre/" rel="noopener noreferrer"&gt;Cover Photo by Alexander Kovalev from Pexels&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mentalhealth</category>
      <category>productivity</category>
      <category>deno</category>
    </item>
    <item>
      <title>PocketBase + SurrealDB</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Fri, 14 Feb 2025 18:27:43 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/pocketbase-surrealdb-2ilo</link>
      <guid>https://dev.to/aaronblondeau/pocketbase-surrealdb-2ilo</guid>
      <description>&lt;p&gt;&lt;a href="https://pocketbase.io/" rel="noopener noreferrer"&gt;PocketBase&lt;/a&gt; is shockingly well engineered.&lt;/p&gt;

&lt;p&gt;PocketBase is an open source backend that provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication (including social auth)&lt;/li&gt;
&lt;li&gt;Database (SQLite, with realtime support)&lt;/li&gt;
&lt;li&gt;File storage (local or S3)&lt;/li&gt;
&lt;li&gt;Dart and Javascript SDKs for above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What makes it so well engineered is how &lt;a href="https://pocketbase.io/docs/go-overview/" rel="noopener noreferrer"&gt;extensible and flexible&lt;/a&gt; it is. It also packs a punch for being so simple and lightweight.&lt;/p&gt;

&lt;p&gt;It does have one core limitation in that it uses SQLite as it's storage engine. This means two things&lt;br&gt;
1) Scaling can only happen vertically.&lt;br&gt;
2) Support for Geo-spatial and vector data types are not available.&lt;/p&gt;

&lt;p&gt;I think that (Turso)[&lt;a href="https://turso.tech/" rel="noopener noreferrer"&gt;https://turso.tech/&lt;/a&gt;] and PocketBase could become good friends. &lt;a href="https://pocketbase.io/docs/go-overview/#github-comtursodatabaselibsql-client-golibsql" rel="noopener noreferrer"&gt;There already is some support for this.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PocketBase's solid design and extensibility also makes it easy to integrate with other backends to access features beyond SQLite. Here is an example scenario. Let's say you're building an app like &lt;a href="https://x-skies.com/" rel="noopener noreferrer"&gt;x-skies&lt;/a&gt; where users can report UFO sightings. Each sighting will have a description as well as a latitude and longitude. You'd like to use &lt;a href="https://surrealdb.com/docs/surrealdb/models/geospatial" rel="noopener noreferrer"&gt;SurrealDB's spatial support&lt;/a&gt; to manage the location data and PocketBase for everything else. The data flow would look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb34adsmx89zcp5kc2rse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb34adsmx89zcp5kc2rse.png" alt="Image description" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Due to PocketBase's excellent set of hooks this is very easy to do! An example with all the code can be found here : &lt;a href="https://github.com/aaronblondeau/surreal_pocket" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/surreal_pocket&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; is to use PocketBase's OnRecordCreateRequest hook to watch each incoming sighting : &lt;a href="https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L139" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L139&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this hook I grab that latitude and longitude that were sent by the frontend and make sure they are stored in the new record's custom data. Since I am not storing the latitude and longitude at all in PocketBase this step ensures those fields don't get lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; is to use PocketBase's OnRecordCreateExecute hook to save the location data in SurrealDB : &lt;a href="https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L161" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L161&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I chose this hook because at this point PocketBase has already assigned an id to the record but not yet saved it. This means I can use the id to save the location data in SurrealDB. If there is an error when saving to Surreal it will also prevent the save from happening in PocketBase to help things stay in sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; is to use the OnRecordEnrich hook to re-attach the latitude and longitude as each record is returned from the server : &lt;a href="https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L180" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L180&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OnRecordEnrich is where the PocketBase crew deserves a high five for their design work because it so incredibly useful. In this callback I simply lookup the matching record in SurrealDB and attach the latitude and longitude before it goes out the door. There was a slight trick to this though:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;e.Record.WithCustomData(true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This WithCustomData call lets me attach my own virtual fields to the records so that they seem like ordinary database fields in the frontend code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt; is to use the OnRecordAfterDeleteSuccess hook to make sure things stay in sync when records are deleted in PocketBase. You'd want a similar rig for updates as well. &lt;a href="https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L204" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L204&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finally&lt;/strong&gt; add a custom route that performs spatial searches in SurrealDB and merges the results with the PocketBase data : &lt;a href="https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L84" rel="noopener noreferrer"&gt;https://github.com/aaronblondeau/surreal_pocket/blob/main/main.go#L84&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now you've got a really clean way to add spatial support to PocketBase!&lt;/p&gt;

&lt;p&gt;Note that I purposefully made this harder than it had to be to test the extensibility of PocketBase. The latitude and longitude could be stored in PocketBase as well as SurrealDB with Surreal only providing the spatial indexing. However, I thought it would be neat to see if I could make those fields fully virtual.&lt;/p&gt;

&lt;p&gt;Don't let PocketBase's simplicity prevent you from trying it out. Adding vector support, full text search, and all kinds of other features are possible with similar techniques.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>pocketbase</category>
      <category>surrealdb</category>
      <category>howto</category>
    </item>
    <item>
      <title>Bow to the rectangle</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Thu, 13 Feb 2025 03:09:25 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/bow-to-the-rectangle-2dg5</link>
      <guid>https://dev.to/aaronblondeau/bow-to-the-rectangle-2dg5</guid>
      <description>&lt;p&gt;Here are the steps I had to take to login to Facebook Business Manager recently:&lt;br&gt;
1) Go to Facebook. I'm logged out again even though I just logged in earlier in the day.&lt;br&gt;
2) Use password from my password manager.&lt;br&gt;
3) Use code from authentication app.&lt;br&gt;
4) Try to go to business manager.&lt;br&gt;
5) It asks to validate business email.&lt;br&gt;
6) Get code from business email and put that in.&lt;br&gt;
7) It says my access is restricted and I have to wait 37 minutes to access business manager. Not sure where they came up with 37 but I have seen all kinds of random wait times with this.&lt;br&gt;
8) Wait the 37 minutes.&lt;br&gt;
9) Get asked for authentication code again.&lt;br&gt;
10) Finally get to business manager, but it took so long I forgot what I was going to do there.&lt;/p&gt;

&lt;p&gt;Logging in to anything these days requires multi-factor authentication. I have come to refer to getting 2FA codes as "&lt;strong&gt;bowing to the rectangle&lt;/strong&gt;". (In my culture, being forced to bow down to something is a demeaning and humiliating experience) &lt;/p&gt;

&lt;p&gt;I totally understand that our tech overlords have to address the problem of people using insecure passwords and hackers stealing browser sessions and so on. However, I get the feeling that something else is going on. What is next for Facebook login steps? Is 3FA (three factor authentication) where they come to my house and swab me for my DNA next time I try to login?&lt;/p&gt;

&lt;p&gt;I have a neighbor who doesn't have a cell phone or a computer. He doesn't have to bow to the rectangle. Yet. I just have this feeling that with the rise of AI and the automation of everything that they will make him bow too. Good luck paying your electricity bill or getting a Dr. appointment without good old two factor authentication.&lt;/p&gt;

&lt;p&gt;I have been thinking about this for several weeks and I have only identified three very imperfect solutions developing applications that don't require onerous authentication:&lt;br&gt;
1) Fully anonymous users&lt;br&gt;
2) Peer to peer applications&lt;br&gt;
3) Public key (Web3/crypto) style authentication&lt;/p&gt;

&lt;p&gt;For solution #1 there is only a small set of applications that you can create with no user authentication. To try my hand at this and to continue the conspiratorial theme of this post I created an app called &lt;a href="https://x-skies.com/" rel="noopener noreferrer"&gt;X-Skies&lt;/a&gt;. X-Skies is an app that lets anyone post or observe Drone and UFO sightings. It was really hard for me to adjust to building an app without a login. Some design work also had to go into figuring out how to prevent abuse of a system where everyone is anonymous.&lt;/p&gt;

&lt;p&gt;For solution #2 there is a much broader range of applications that are possible. Frameworks like &lt;a href="https://socketsupply.co/" rel="noopener noreferrer"&gt;Socket Supply Co&lt;/a&gt; and tools like &lt;a href="https://peerjs.com/" rel="noopener noreferrer"&gt;PeerJS&lt;/a&gt; are also available. However designing a peer to peer application that is useful and accessible to ordinary users is going to be very hard.&lt;/p&gt;

&lt;p&gt;If you can't get ordinary folks to even use secure passwords without losing them then solution #3 is a real long shot.&lt;/p&gt;

&lt;p&gt;So, where does this road take us. Will we have to sacrifice our privacy and dignity by being forced to use more and more invasive authentication methods. My iPhone already has my face. What will it want next?&lt;/p&gt;

&lt;p&gt;I think that we as developers are going to need to find some innovative ways to serve people with technology without demeaning them. If we don't we'll wind up with &lt;a href="https://www.gotquestions.org/mark-beast.html" rel="noopener noreferrer"&gt;the mark&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pexels.com/photo/grey-bullet-camera-274895/]" rel="noopener noreferrer"&gt;Cover Photo by Pixabay from Pexels&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>security</category>
      <category>web3</category>
    </item>
    <item>
      <title>I'm not going to learn a new programming language this year</title>
      <dc:creator>aaronblondeau</dc:creator>
      <pubDate>Thu, 02 Jan 2025 23:43:34 +0000</pubDate>
      <link>https://dev.to/aaronblondeau/im-not-going-to-learn-a-new-programming-language-this-year-12kj</link>
      <guid>https://dev.to/aaronblondeau/im-not-going-to-learn-a-new-programming-language-this-year-12kj</guid>
      <description>&lt;p&gt;This year I am going to learn something entirely different: knitting.  With yarn and needles. Like a little old lady.&lt;/p&gt;

&lt;p&gt;For the past 2 decades I have made a goal of learning a new programming language each year. I've learned languages like Elixir, Gleam, D, Ruby, C#, Dart, PHP, Java, Groovy, Scala, Kotlin, Swift, Objective-C, and Perl.&lt;/p&gt;

&lt;p&gt;I never used most of them. Learning new languages is not a waste of time even if they go unused. Seeing how other people think and design code is extremely valuable. I truly enjoyed diving into functional programming with Elixir and Gleam. I see ideas from that paradigm appearing my TypeScript all the time now. However, for the near term I have simply hit a point where there is too much diminishing return in learning a new language.&lt;/p&gt;

&lt;p&gt;This year I am going to learn to knit because my brain is a mess. The feeds and the algorithms and the AI autocomplete and the constant news cycle are all running rampant. I can hardly focus. Productivity is hard, getting into flow state is hard. Finishing this post without watching something on YouTube or checking the news is hard. That's enough.&lt;/p&gt;

&lt;p&gt;I am going to spend time disconnecting and doing something boring with my hands so that I can start to reclaim my mind. Thankfully my daughters are all kitting and crocheting experts so I don't have to go crawling right back to YouTube to learn it. I'll get to spend more time with them and they'll get to practice teaching their hard won skills to me.&lt;/p&gt;

&lt;p&gt;My hope is that by spending this year finding ways to reclaim my ability to focus I will become a better developer and a better person.&lt;/p&gt;

&lt;p&gt;Here is my work so far. Looks kind of like my code: full of holes and knots. I'll post updates here as I make progress!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96lvfn0deimpfqy8ux8o.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96lvfn0deimpfqy8ux8o.JPG" alt="Knitting Begins" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mentalhealth</category>
      <category>developer</category>
      <category>productivity</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
