<?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>Forem: Steve Frank</title>
    <description>The latest articles on Forem by Steve Frank (@lardcanoe).</description>
    <link>https://forem.com/lardcanoe</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%2F17935%2Fdfb069ff-b897-4103-b8ed-8ee36bea853a.jpeg</url>
      <title>Forem: Steve Frank</title>
      <link>https://forem.com/lardcanoe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lardcanoe"/>
    <language>en</language>
    <item>
      <title>Claude was finally smarter than me</title>
      <dc:creator>Steve Frank</dc:creator>
      <pubDate>Thu, 26 Feb 2026 14:02:06 +0000</pubDate>
      <link>https://forem.com/lardcanoe/claude-was-finally-smarter-than-me-305k</link>
      <guid>https://forem.com/lardcanoe/claude-was-finally-smarter-than-me-305k</guid>
      <description>&lt;p&gt;Elixir, Phoenix, Ash project&lt;/p&gt;

&lt;p&gt;Got 100's of text fields spread across 20+ Ash Resources. Finally got around to enforcing some sane max_length constraints on them.&lt;/p&gt;

&lt;p&gt;Simple prompt, "Add sane max_length constraints to all Ash Resource text fields, based on their implied meaning"&lt;/p&gt;

&lt;p&gt;Few minutes later, it was done, and after a quick review there weren't any changes needed. Next up, add that same max_length as &lt;code&gt;maxlength&lt;/code&gt; on the corresponding inputs fields.&lt;/p&gt;

&lt;p&gt;Simple prompt: "Now apply those max_length constraints to the field's corresponding input boxes"&lt;/p&gt;

&lt;p&gt;Which I thought I was going to get 100+ edits. Well, wrong. It thought for 30s or so, and ultimately made a nice simple change in one place, core_components.ex&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  def input(%{field: %FormField{} = field} = assigns) do
    errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []

    assigns
    |&amp;gt; assign(field: nil, id: assigns.id || field.id)
    ...
    |&amp;gt; maybe_inject_maxlength(field)
    |&amp;gt; input()
  end

  ...

  @maxlength_types ~w(text email tel url search textarea)

  defp maybe_inject_maxlength(%{type: type, rest: rest} = assigns, field)
       when type in @maxlength_types and not is_map_key(rest, :maxlength) do
    case get_field_max_length(field) do
      nil -&amp;gt; assigns
      max_length -&amp;gt; %{assigns | rest: Map.put(rest, :maxlength, max_length)}
    end
  end

  defp maybe_inject_maxlength(assigns, _field), do: assigns

  defp get_field_max_length(%FormField{} = field) do
    with %{source: %AshPhoenix.Form{resource: resource}} &amp;lt;- field.form,
         %{constraints: constraints} when is_list(constraints) &amp;lt;-
           Info.attribute(resource, field.field) do
      Keyword.get(constraints, :max_length)
    else
      _ -&amp;gt; nil
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Absolutely brilliant. I didn't even think of that. It used one of Ash's superpowers, the DSL introspection, to just make this work.&lt;/p&gt;

&lt;p&gt;24hrs later I am still dazed. Been using ChatGPT for 2 years or so, Claude for 9 months. And this is maybe only the 3rd time that I was actually left a bit stunned. Like, either I'm losing it, or it really is just better than me.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>agents</category>
      <category>elixir</category>
      <category>ash</category>
    </item>
    <item>
      <title>Sanity Checks</title>
      <dc:creator>Steve Frank</dc:creator>
      <pubDate>Mon, 05 Jan 2026 14:57:21 +0000</pubDate>
      <link>https://forem.com/lardcanoe/sanity-checks-ci0</link>
      <guid>https://forem.com/lardcanoe/sanity-checks-ci0</guid>
      <description>&lt;h2&gt;
  
  
  Weird things happen in prod
&lt;/h2&gt;

&lt;p&gt;I don't care how much test coverage you have, or how many database transactions you use, or how many years you have been coding: &lt;strong&gt;weird things happen in prod.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scripts that perform sanity checks on your databases will catch these oddities before they become a major issue. Or at least, that's the idea, and it has been true for the past two decades for me.&lt;/p&gt;

&lt;p&gt;Any time you introduce business rules that can't (or not easily) be enforced directly by the database, add a basic check that scans for records that are not following those rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1: Basic attribute values
&lt;/h3&gt;

&lt;p&gt;Rule: When &lt;code&gt;state == 'paid'&lt;/code&gt; then &lt;code&gt;paid_at&lt;/code&gt; should not be null.&lt;/p&gt;

&lt;p&gt;Simply write a query that selects all records &lt;code&gt;where state == 'paid' and paid_at is null&lt;/code&gt; and send them to your alerting system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: X-Tenancy Mismatch
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Object X&lt;/code&gt; belongs_to &lt;code&gt;Object Y&lt;/code&gt; and both should have an &lt;code&gt;Owner Z&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Maybe this check alerts PagerDuty since it could be disastrous.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Examples:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;All users belong to at least 1 organization&lt;/li&gt;
&lt;li&gt;All organizations have at least 1 user&lt;/li&gt;
&lt;li&gt;Users set to super admin only belong to Org Foo (maybe this one runs every 5min)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Additional Tips:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Feel free to limit checks to records that have been created or updated in the past 48 hours&lt;/li&gt;
&lt;li&gt;Ignore archived records if applicable&lt;/li&gt;
&lt;li&gt;Run the checks manually before deploying to prevent a flood of errors for bad sanity check logic. Sometimes I forget about an edge case that is actually valid.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>My Failed Student Housing App</title>
      <dc:creator>Steve Frank</dc:creator>
      <pubDate>Tue, 23 Apr 2024 16:53:55 +0000</pubDate>
      <link>https://forem.com/lardcanoe/my-failed-student-housing-app-2k7p</link>
      <guid>https://forem.com/lardcanoe/my-failed-student-housing-app-2k7p</guid>
      <description>&lt;p&gt;tldr; I have a moderate sized PoC elixir code base collecting dust that will never be used again, and hopefully it has something someone will need.&lt;/p&gt;

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

&lt;p&gt;I've been looking into different markets for my next startup. One that stood out was Student Housing software for colleges and universities. What intrigued me initially was that &lt;em&gt;the #1 company bought #2 and #3 in 2022&lt;/em&gt;, so the options for colleges dwindled to just a few. And the #1 can now charge 4-5x resulting in 6 figure yearly bills!&lt;/p&gt;

&lt;p&gt;Certainly I can build something that I could charge significantly less that would benefit the few thousand smaller colleges that couldn't afford that price gouging.&lt;/p&gt;

&lt;p&gt;While I was deep in customer discovery, I figured I would also build out a PoC to show directors at the school to let them know I deeply understood the problems they faced. If they could see that, then maybe they would give me a shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Elixir&lt;/li&gt;
&lt;li&gt;Phoenix + LiveView&lt;/li&gt;
&lt;li&gt;TailwindCSS (with &lt;a href="https://flowbite.com/" rel="noopener noreferrer"&gt;Flowbite&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Postgres&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ash-hq.org/" rel="noopener noreferrer"&gt;Ash&lt;/a&gt; (2.0, 3.0 wasn't out yet)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I always need to be learning, so I took the chance to finally learn Ash. Something about it screamed "Yes!" to me.&lt;/p&gt;

&lt;p&gt;My previous passion project was &lt;a href="https://clickduel.com" rel="noopener noreferrer"&gt;ClickDuel&lt;/a&gt; and I used that to really dive into Elixir and LiveView. Take a look, my kids and I still love using it. So the housing app was my chance to take a deep dive into Ash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ash
&lt;/h2&gt;

&lt;p&gt;As with any new tool, there is a ramp up period. Ash was no different. During the 4 months I worked on this project, as I struggled with doing some basic stuff (like Phoenix Forms using Ash) I kept asking myself, "Is Ash &lt;em&gt;really&lt;/em&gt; gonna make me faster in the long run?" Now that I am on 2 other new projects, both using the same stack as above, I can confidently say yes. I am glad I had the time to learn Ash. The speed at which I got the new stuff up was crazy fast. And 3.0 introduces domains, which encapsulate code so much better.&lt;/p&gt;

&lt;p&gt;I have used ORMs in PHP, C#, Ruby, and NodeJS, so I am well versed in the problems of figuring out your domain and data structures and mapping that to the tool du jour. Ash is not an ORM, but it serves the problem, and I like Ash's approach so much better than others.&lt;/p&gt;

&lt;p&gt;The team is really responsive answering questions in their &lt;a href="https://elixirforum.com/c/ash-framework-forum/123" rel="noopener noreferrer"&gt;forum&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For my deployment model, every school would need to be isolated in the database, and Ash's multi-tenancy approach made this dead-a$$ simple to implement. Like, a few lines of code.&lt;/p&gt;

&lt;p&gt;Ash also has a powerful policy system to enforce access to data, but it was just too much to learn, so I constantly just disabled it.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Defined Schemas
&lt;/h2&gt;

&lt;p&gt;The thing with Student Housing is that an immense amount of it is based on dozens of forms. A form to request housing, a form for your profile, a form to be an RA, a form to switch rooms, a form to submit a maintenance ticket. You get the picture.&lt;/p&gt;

&lt;p&gt;But the problem is that every single school has their own fields and structure and workflow for these forms. So I went with our buddy, JSON Schema. There is a decent chunk of code dealing with that. Chances are, I ran into the same problem as you, if you're using JSON Schema...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fwkl1w4ei2vfkhq33glv7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fwkl1w4ei2vfkhq33glv7.png" alt="Forms"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other tools
&lt;/h2&gt;

&lt;p&gt;Don't reinvent the wheel!&lt;/p&gt;

&lt;h3&gt;
  
  
  Flowbite
&lt;/h3&gt;

&lt;p&gt;One of the things that I am happy to drop $ on are good looking UI components. Flowbite does a great job with theirs. Dark mode just works. Finally got a good looking sidebar to quickly view an item; always wanted to implement that. Though I will say their dropdown logic is flaky.&lt;/p&gt;

&lt;h3&gt;
  
  
  AG Grid
&lt;/h3&gt;

&lt;p&gt;I made extensive use of &lt;a href="https://ag-grid.com/" rel="noopener noreferrer"&gt;AG Grid&lt;/a&gt;. There are LiveView hooks and whatnot showing how I loaded data, and stylized different types like bools, links, and status.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/josdejong/jsoneditor" rel="noopener noreferrer"&gt;JSON Editor&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This provided a simple drop-in to edit JSON Schemas. Visual drag n drop was out of scope for the PoC, though I did try some out. O' lord did I try them out. Days of my life I want back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fatbqq1xpeqhvnmpdetld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fatbqq1xpeqhvnmpdetld.png" alt="Profile fields"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://hexdocs.pm/ex_json_schema/0.10.2/readme.html" rel="noopener noreferrer"&gt;ExJsonSchema&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Great JSON schema validator.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/SortableJS" rel="noopener noreferrer"&gt;SortableJS&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This allowed me to provide a way to sort items in the UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F9yh72eat2thplij2x12c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F9yh72eat2thplij2x12c.png" alt="Sorting"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://react-querybuilder.js.org/" rel="noopener noreferrer"&gt;react-querybuilder&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;There is no chance I am using this properly. I don't understand React at all (I'm a Vue dev). But I needed to allow users to build complex queries, and this is a no-brainer tool to use for that purpose. The challenge was using it in LiveView.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSX
&lt;/h3&gt;

&lt;p&gt;I think it was just changing to &lt;code&gt;--target=es2020&lt;/code&gt; and I was able to use &lt;code&gt;.jsx&lt;/code&gt; files alongside &lt;code&gt;.js&lt;/code&gt; which made some of the Javascript work easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finally, the code
&lt;/h2&gt;

&lt;p&gt;Please keep in mind that this was implemented as a proof of concept. I just needed to get some stuff working, and not necessarily working well, and certainly not with any tests.&lt;/p&gt;

&lt;p&gt;Many times, I went with whatever approach worked, and moved on with life, so please be mindful of that if you browse the code. Though, if I did something stupid, please let me know so I can fix it in future projects I work on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lardcanoe/housing-app" rel="noopener noreferrer"&gt;https://github.com/lardcanoe/housing-app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If I get 10 likes, or some comments, I'll take the time to document how to get it running locally. It's should be pretty typical for an elixir app though...&lt;/p&gt;

&lt;p&gt;I am more than happy to expand on any topic above, or others. Just leave a comment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fr5ydvz1cako58mowa4nc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fr5ydvz1cako58mowa4nc.png" alt="Example of Settings"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>liveview</category>
      <category>startup</category>
      <category>ash</category>
    </item>
    <item>
      <title>Migrate from Heroku to Azure</title>
      <dc:creator>Steve Frank</dc:creator>
      <pubDate>Fri, 17 Dec 2021 18:40:49 +0000</pubDate>
      <link>https://forem.com/lardcanoe/migrate-from-heroku-to-azure-1c9g</link>
      <guid>https://forem.com/lardcanoe/migrate-from-heroku-to-azure-1c9g</guid>
      <description>&lt;ul&gt;
&lt;li&gt;The overall goal was to get our staging site running 100% in Azure. This will all work for production, but you'll want to first do the work for a dev/qa/staging environment, and then use something like terraform to deploy production.&lt;/li&gt;
&lt;li&gt;I went into this with zero experience in Azure. Please leave a comment for anything I should fix.&lt;/li&gt;
&lt;li&gt;I'm writing this blog a week after I got things working, and I didn't take notes, so some parts are light on details.&lt;/li&gt;
&lt;li&gt;After a few days into the work, I truly understood the simplicity that I get from Heroku, and the extra $ you spend is well worth it if you don't have a dedicated DevOps team.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Heroku
&lt;/h1&gt;

&lt;p&gt;Here's our setup in Heroku:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nodejs v14

&lt;ul&gt;
&lt;li&gt;Dynos for Web Requests&lt;/li&gt;
&lt;li&gt;Dynos for background workers&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Postgres + Redis&lt;/li&gt;

&lt;li&gt;VueJS frontend&lt;/li&gt;

&lt;li&gt;Custom domain, taking advantage of Heroku managing certificates&lt;/li&gt;

&lt;li&gt;CodeshipCI for builds&lt;/li&gt;

&lt;li&gt;Heroku Scheduler for cron&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Example Procfile:&lt;/p&gt;

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

release: node_modules/.bin/sequelize db:migrate &amp;amp;&amp;amp; node_modules/.bin/sequelize db:seed:all
web: node --optimize_for_size --max_old_space_size=1048576 index.js
worker: env WEB_MEMORY=1024 WEB_CONCURRENCY=2 node --optimize_for_size --max_old_space_size=1048576 worker.js
worker-pm: env WEB_MEMORY=2048 WEB_CONCURRENCY=2 node --optimize_for_size --max_old_space_size=2621440 worker.js
worker-p14gb: env WEB_MEMORY=8192 WEB_CONCURRENCY=2 node --optimize_for_size --max_old_space_size=8388608 worker.js


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

&lt;/div&gt;
&lt;h1&gt;
  
  
  Azure
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;I was given an Azure subscription to use, so I don't have instructions or experience w/ signup&lt;/li&gt;
&lt;li&gt;I was nudged to use a Web App (even though when I was told this I had no clue what that meant), so that was the path I took.&lt;/li&gt;
&lt;li&gt;In Heroku, we have 2 independently scalable systems: web and workers. I want to keep that when moving to Azure, thus each will have its own Web App.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Azure Resources
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Notes:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keep track of the region you start with and make sure to create them all in the same region.&lt;/li&gt;
&lt;li&gt;There is an App Insights feature that I didn't look into, but I did enable it on everything I created. Probably easier to do it when creating instead of later...&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Provisioning:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a resource group for all the infrastructure to live in&lt;/li&gt;
&lt;li&gt;Create a Web App for each service; in our case we'll have one for web and one for workers. As part of the creation, you will need to create a Web App Plan for each, which is basically the same as the dyno type from Heroku. Choose one similar to what you had. (You can change the size and type later if you get this wrong.)

&lt;ul&gt;
&lt;li&gt;Just like with Heroku, you'll be able to tail logs and ssh in to help with debugging and monitoring.&lt;/li&gt;
&lt;li&gt;I've only used the portal to do this; I have not tried the azure cli yet. You'll see "SSH" and "Stream Logs" in the left menu of the selected Web App.&lt;/li&gt;
&lt;li&gt;Under Settings / Configuration, add a new Application Setting: &lt;code&gt;OPENSSL_CONF&lt;/code&gt; = &lt;code&gt;/etc/ssl/&lt;/code&gt; to deal with openssl 1.1.1.d failing on the eventually provisioned web app containers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Provision a new &lt;code&gt;Azure Database for PostgreSQL&lt;/code&gt;, appropriately sized and the correct version (or use latest). When and how to load the data from Heroku's database is up to you. Heroku's managed postgres databases don't give access to replicate, which is why I performed a final backup and then a restore. I'm also moving over our staging site so I can be a bit more cavalier with the process:

&lt;ul&gt;
&lt;li&gt;Perform a backup, and then use &lt;code&gt;pg_restore&lt;/code&gt; to perform the restoration into the new Azure instance. If this was production, you'd want to put your app in maintenance mode before doing that backup.&lt;/li&gt;
&lt;li&gt;On the Networking page of the new instance, you can grant your home's IP access to the db.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pg_restore --verbose --no-owner -h &amp;lt;server name&amp;gt;.postgres.database.azure.com -U psqladmin -d &amp;lt;db_name&amp;gt; heroku_database.dump&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Note: If your provisioned username contains an &lt;code&gt;@&lt;/code&gt; like &lt;code&gt;pgadmin@servername&lt;/code&gt; and you need to use that in a URI connection string, then encode it to &lt;code&gt;pgadmin%40servername&lt;/code&gt;, so the resulting connection string is &lt;code&gt;postgres://pgadmin%40servername:password@servername:port/dbname&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Provision &lt;code&gt;Azure Cache for Redis&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;The Access Keys page has the connection string info&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Provision a Key Vault, and add your secrets

&lt;ul&gt;
&lt;li&gt;We'll use this for very secret stuff.&lt;/li&gt;
&lt;li&gt;You can run &lt;code&gt;heroku config -j --app &amp;lt;name&amp;gt;&lt;/code&gt; to get a JSON dump of your secrets from Heroku.&lt;/li&gt;
&lt;li&gt;There are a few ways to get secrets to your Web Apps:

&lt;ul&gt;
&lt;li&gt;You can browse to the Web App, go to the Configuration page. and manually add or import the json (make sure to merge and not replace since Azure puts some settings there.)&lt;/li&gt;
&lt;li&gt;Or you can have the release pipeline we'll make later pull settings from the key vault and push them into the Web App.&lt;/li&gt;
&lt;li&gt;For simplicity, I went with just merging them into the Web Apps by hand (though I did use the Advanced method that exposes the JSON that can be cut/pasted).&lt;/li&gt;
&lt;li&gt;Web Apps have a special section for storing connection strings (like for the database or redis) and for simplicity of this initial transition I chose to skip using it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Azure DevOps - Build Pipeline
&lt;/h2&gt;

&lt;p&gt;We're now going to replicate most of the magic that happens when you &lt;code&gt;git push&lt;/code&gt; to Heroku.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Note: At pretty much every step going forward I will just assume you'll press the "Save" button.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Switch over to Azure Devops, &lt;a href="https://dev.azure.com/" rel="noopener noreferrer"&gt;https://dev.azure.com/&lt;/a&gt;, and create a project if you don't have one yet.&lt;/li&gt;
&lt;li&gt;Push up your existing code. You're adding a new remote in your local git repo so you can still &lt;code&gt;git push heroku main&lt;/code&gt; as well as now &lt;code&gt;git push azure main&lt;/code&gt; (or whatever you call the remotes).&lt;/li&gt;
&lt;li&gt;Now we need a new Build pipeline. This is all the magic that Heroku does for you in their buildpacks. Create an &lt;code&gt;azure-pipelines.yml&lt;/code&gt; file in the root of your repo and push that up to Azure so you can create the Build pipeline from it.&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This pipeline handles a few things: 

&lt;ul&gt;
&lt;li&gt;Caching two different npm's (i.e. node_modules) because the backend and client side are in the same repo.&lt;/li&gt;
&lt;li&gt;Installing postgres and redis as services so our unit tests have real servers to work against.&lt;/li&gt;
&lt;li&gt;It handles running and publishing mocha unit tests.&lt;/li&gt;
&lt;li&gt;Dealing with phantomjs nonsense since the build image contains phantomjs so &lt;code&gt;npm install&lt;/code&gt; doesn't download the binary, thus later preventing my Web App from having phantomjs. This was a full day of my life wasted, so you're welcome to anyone who else stumbles on this craziness.&lt;/li&gt;
&lt;li&gt;And finally, we zip everything up and publish it, which I guess is similar to Heroku generating a 250MB+ slug. I take an extremely simplistic approach and zip it all. You could exclude tests for example. &lt;strong&gt;ALL&lt;/strong&gt; of this will wind up on your Web App, so if you really can't have certain data on public facing servers, this is your chance to prune before archiving.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Azure DevOps - Release Pipeline
&lt;/h2&gt;

&lt;p&gt;Now that we have the Build generating a giant zip file containing our entire codebase + node_modules + any static files created from &lt;code&gt;npm build&lt;/code&gt; of our client, we can now deploy it to our Web Apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I'd love to share a json file to import the pipeline, but the one it generates is like 20k lines long and contains tons of unique guids, so instructions and screenshots it is!&lt;/li&gt;
&lt;li&gt;Final pipeline: &lt;img src="https://media.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%2Fcsosy90wqllbltzo1i08.png" alt="Image description"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Create a new empty Release pipeline&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Artifacts
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add an artifact
&lt;img src="https://media.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%2Fmbotftcjxg7486p1i2mz.png" alt="Screenshot"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Variables
&lt;/h3&gt;

&lt;p&gt;Our database migration task below needs access to our database to run migrations and seeds. This lets us bring in secrets from our vault.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch over to Variables and then select "Variable groups"
&lt;img src="https://media.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%2Fz2uzy8284wvxi78qwu9r.png" alt="Screenshot"&gt; &lt;/li&gt;
&lt;li&gt;Click "Manage variable groups" and bring in the vars you need from the vault.

&lt;ul&gt;
&lt;li&gt;I don't get why some things can use underscores and others can't, so you'll see in my examples stuff named &lt;code&gt;DB-HOSTNAME&lt;/code&gt; whereas the env var I need is &lt;code&gt;DB_HOSTNAME&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click "Link variable group"

&lt;ul&gt;
&lt;li&gt;I didn't realize I needed to do this and lost a few hours wondering why my variables weren't accessible. I want to give some Azure PM the evil-eyes for this.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Switch to "Pipeline Variables" and add what you need.
&lt;img src="https://media.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%2F97has6bntujcvp6so87i.png" alt="Screenshot"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: When you are done with the next few steps you can come back and scope variables to certain stages. &lt;/p&gt;

&lt;h3&gt;
  
  
  Database Migration Stage
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a new stage call "Database Migration"
&lt;img src="https://media.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%2Fsl0e8425dj66f1kssxht.png" alt="Screenshot"&gt;
&lt;/li&gt;
&lt;li&gt;Click "Agent Job" and change Specification to "ubuntu-latest"&lt;/li&gt;
&lt;li&gt;Create an "Extract Files" tasks
&lt;img src="https://media.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%2Fcf1sa7x05dpgme2dr0z1.png" alt="Screenshot"&gt;
&lt;/li&gt;
&lt;li&gt;Create a Node Installer tasks (just in case)
&lt;img src="https://media.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%2F7zg13nkonnn96408om56.png" alt="Screenshot"&gt;
&lt;/li&gt;
&lt;li&gt;Create a Bash task. You'll need to tweak env vars to what your app expects
&lt;img src="https://media.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%2Fkal9tgxpfhc81xlknj13.png" alt="Screenshot"&gt;
&lt;/li&gt;
&lt;li&gt;Create another Bash task (yes, you could just &amp;amp;&amp;amp; with the task above instead of making a new one. I like having it be clear exactly which part broke.)
&lt;img src="https://media.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%2Fr2ur915laogshgevenij.png" alt="Screenshot"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Deploy Stages
&lt;/h3&gt;

&lt;p&gt;We are going to deploy to web and workers in parallel.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;This stage's agent can run on windows-latest
&lt;img src="https://media.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%2Fjfhig4e4dxtrn16jn7px.png" alt="Screenshot"&gt;

&lt;ul&gt;
&lt;li&gt;Startup command&lt;/li&gt;
&lt;li&gt;The image Azure uses doesn't work with phantomjs, so we need to install some packages, and unzip our cached phantomjs from the build.&lt;/li&gt;
&lt;li&gt;Again, you're welcome for knowing how to install custom fonts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

apt-get update -qq &amp;amp;&amp;amp; apt-get install libfontconfig libssl-dev -yqq &amp;amp;&amp;amp; cp -r /home/site/wwwroot/.fonts /root/ &amp;amp;&amp;amp; unzip -oqq /home/site/wwwroot/bin/phantomjs.zip -d /home/site/wwwroot/bin &amp;amp;&amp;amp; node --optimize_for_size --max_old_space_size=2621440 index.js


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

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Deploy Azure App Service web
&lt;img src="https://media.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%2Fsaq9pppa8zx3v9em8ssq.png" alt="Screenshot"&gt; &lt;/li&gt;
&lt;li&gt;Now duplicate this stage for workers. You may need to tweak the js file used to start.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a Release
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.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%2Ftybs208x8gxc8dpc0bbp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Ftybs208x8gxc8dpc0bbp.png" alt="Screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Cron
&lt;/h1&gt;

&lt;p&gt;I'll give migrating the Heroku Scheduler to Azure its own section. Since I used Linux Web Apps for everything, I guess I'm not allowed to create WebJobs? Kinda lame. Anyway, I saw a few approaches, most notably installing cron on your Web App during startup. Since I have more than 1 instance running, I don't want cron running more than once. I chose to use a Function App which allows me to define a function that runs on a schedule. This turned out to be rather useful.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provision a new Function App&lt;/li&gt;
&lt;li&gt;Create a new function, e.g. &lt;code&gt;DailyTrigger&lt;/code&gt;, with a schedule like &lt;code&gt;0 0 6 * * *&lt;/code&gt; to run daily at 6am UTC.&lt;/li&gt;
&lt;li&gt;Write some code to do what you need. I went with C# for simplicity. Triggering cron for us is simply a POST to an internal endpoint. (There is code to ignore the cert because I haven't implemented SSL certificates in my Web App yet.)&lt;/li&gt;
&lt;/ol&gt;

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

    using (var httpClientHandler = new System.Net.Http.HttpClientHandler())
      {
        httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =&amp;gt; {
          return true;
        };

        var webhookURL = "https://....";
        using var client = new System.Net.Http.HttpClient(httpClientHandler);
        client.DefaultRequestHeaders.Add("Authorization", "********");
        client.GetAsync(webhookURL).Wait();
      }

      log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");


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

&lt;/div&gt;

</description>
      <category>heroku</category>
      <category>azure</category>
      <category>migrate</category>
    </item>
    <item>
      <title>Multi-tenancy Leaks IRL</title>
      <dc:creator>Steve Frank</dc:creator>
      <pubDate>Thu, 17 Sep 2020 17:39:54 +0000</pubDate>
      <link>https://forem.com/lardcanoe/multi-tenancy-leaks-irl-4c4p</link>
      <guid>https://forem.com/lardcanoe/multi-tenancy-leaks-irl-4c4p</guid>
      <description>&lt;p&gt;The dozen or so articles and threads I've read over the years regarding implementing multi-tenancy in SaaS platforms fail to cover areas outside of the database itself. When it comes to a cross-tenant data leak, chances are it won't be an issue at the database level that gets you.&lt;/p&gt;

&lt;h1&gt;
  
  
  Cache Layer
&lt;/h1&gt;

&lt;p&gt;Most architectures have a caching layer (e.g. Redis) in use for all sorts of purposes. Chances are you are not deploying a redis/memcache server for each tenant, so you're probably using the common practice of putting the unique tenant id in the key, maybe at the beginning, such as "1234:users" for tracking users of each tenant.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;What happens if the variable used to put the tenant is wrong? Such as an undefined variable in JS: &lt;code&gt;${teantId}:users&lt;/code&gt; instead of &lt;code&gt;${tenantId}:users&lt;/code&gt;. Subtle typo, but it will work, and produce &lt;code&gt;undefined:users&lt;/code&gt; as the key that all your tenants will access. Well, there's a cross-tenant leak that your days/weeks of obsessing over your database design didn't capture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Or maybe you have the correct variables, but in the wrong order: &lt;code&gt;${accountId}:${tenantId}:users&lt;/code&gt; instead of &lt;code&gt;${tenantId}:${accountId}:users&lt;/code&gt;. Good luck catching either of these in a code review!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A basic way to prevent these issues is to have the tenantId passed in as a param to all cache layer requests. Don't just directly access redis. Instead, create some interface to abstract away redis and force the tenantId to be passed in, and then validate the value.&lt;/p&gt;

&lt;h1&gt;
  
  
  External Services
&lt;/h1&gt;

&lt;p&gt;You can't just obsess over a leak in your code... you need to guard against an external service having their own leak as well! Yes, this happens, even for major cloud providers. Oh the stories some of us can tell.&lt;/p&gt;

&lt;p&gt;Anyway, if the response from the external call provides the tenant identifier somewhere then check that it is what you expected. Otherwise, if you just take it as is, you've just possibly polluted your database with someone else's data, and that isn't gonna be easy to clean up.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Added bonus to anyone who goes back to their code and makes sure their API includes a field in the body or headers to identify the tenant for the response.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrap up
&lt;/h1&gt;

&lt;p&gt;Be paranoid! Everyone is out to get you. And chances are, the place you obsessed the most about multi-tenancy issues isn't gonna be the place that ultimately burns you. Put guards and alerts in your code if there's even a hint of a leak. And remember, if you don't store data then it can't be leaked. :-)&lt;/p&gt;

</description>
      <category>multitenancy</category>
    </item>
  </channel>
</rss>
