<?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: arnu515</title>
    <description>The latest articles on DEV Community by arnu515 (@arnu515).</description>
    <link>https://dev.to/arnu515</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%2F406164%2F7a619321-9edf-458c-bd4f-76a540668cd8.png</url>
      <title>DEV Community: arnu515</title>
      <link>https://dev.to/arnu515</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arnu515"/>
    <language>en</language>
    <item>
      <title>Using DigitalOcean Droplets as Ephemeral Sandboxes for AI Agents</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Mon, 25 May 2026 13:19:29 +0000</pubDate>
      <link>https://dev.to/arnu515/using-digitalocean-droplets-as-ephemeral-sandboxes-for-ai-agents-309k</link>
      <guid>https://dev.to/arnu515/using-digitalocean-droplets-as-ephemeral-sandboxes-for-ai-agents-309k</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://www.digitalocean.com/resources/articles/agentic-ai" rel="noopener noreferrer"&gt;Agentic AI&lt;/a&gt; is a type of artificial intelligence system powered by large language models that operates autonomously using tools to interact with the outside system under minimal human supervision. These AI Agents use &lt;a href="https://www.digitalocean.com/community/tutorials/llm-tool-calling-managed-database-gradient-ai-platform" rel="noopener noreferrer"&gt;&lt;strong&gt;tool calling&lt;/strong&gt;&lt;/a&gt; to interact with the outside world and perform tasks on the user's behalf. These tasks can include programming (including performing long-running programming tasks, such as Ralph loops), sending emails, responding to some event, executing code, searching the web, using a computer, and generally anything else that can be represented as a tool call. While this provides LLMs with great utility, it also gives them destructive power that can result in the deletion of files, execution of harmful code, heavy usage of resources, etc.&lt;/p&gt;

&lt;p&gt;This is where sandboxing is useful: if AI agents could run in an ephemeral machine where making irreversible changes to the system doesn't have many consequences, it can alleviate some of the pitfalls of providing LLMs with this much power. &lt;/p&gt;

&lt;p&gt;Sandboxes are &lt;em&gt;ephemeral&lt;/em&gt; machines that are easy and fast to create and destroy, and can also run for long amounts of time. They’re usually deployed on some cloud provider, since cloud machines are easy to spin up and down, can run for long amounts of time, and can be created with various hardware configurations; but there are tools that allow you to create sandboxes on your local machine (you can use docker for this, for example). Thus, this is a great use case for &lt;a href="https://www.digitalocean.com/solutions/vps-hosting" rel="noopener noreferrer"&gt;Virtual Private Servers (VPS)&lt;/a&gt; such as &lt;a href="https://www.digitalocean.com/products/droplets" rel="noopener noreferrer"&gt;DigitalOcean Droplets&lt;/a&gt;. They can be created and removed with ease, while being &lt;a href="https://www.digitalocean.com/blog/dropletplans-persecbilling-byoip-natgateway" rel="noopener noreferrer"&gt;billed by the second&lt;/a&gt;, so you only pay for what you use. Sandboxes are meant to be ephemeral, i.e. when you delete the sandbox, it loses all its data. Sandboxes are created based on an image, so they start off with a set configuration (such as installed and configured programs, users, keys, etc.).&lt;/p&gt;

&lt;p&gt;In this article, you will learn how you can use DigitalOcean Droplets to create ephemeral sandboxes (which can also have persistent storage attached). You'll be introduced to various DigitalOcean services that make using Droplets as sandboxes easier. Using DO over other tools lets you select the location, compute power, and software installed on the sandbox, while being able to take advantage of DigitalOcean's competitive pricing and connecting other services like &lt;a href="https://www.digitalocean.com/products/block-storage" rel="noopener noreferrer"&gt;Volumes&lt;/a&gt;, &lt;a href="https://www.digitalocean.com/products/vpc" rel="noopener noreferrer"&gt;VPCs&lt;/a&gt;, the &lt;a href="https://www.digitalocean.com/products/container-registry" rel="noopener noreferrer"&gt;Container Registry&lt;/a&gt;, &lt;a href="https://www.digitalocean.com/products" rel="noopener noreferrer"&gt;and more&lt;/a&gt; to your sandbox right in one place.&lt;/p&gt;

&lt;h1&gt;
  
  
  Key Takeaways
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolated Environments for Agentic AI:&lt;/strong&gt; Running autonomous AI tools in isolated cloud environments mitigates security risks like accidental file deletion, resource draining, or malicious script execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Efficiency via Per-Second Billing:&lt;/strong&gt; Services like DigitalOcean Droplets support per-second billing, enabling rapid programmatic provisioning and teardown without incurring flat hourly or monthly minimums.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative Infrastructure Setup:&lt;/strong&gt; Utilizing &lt;code&gt;cloud-init&lt;/code&gt; configurations provides a robust, code-defined method to inject security rules, users, and environment packages during instance launch without pre-building custom disk images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data State Management:&lt;/strong&gt; Combining ephemeral compute with network-attached storage volumes allows you to persist structural project histories or git trees across individual sandbox replacements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Prerequisites
&lt;/h1&gt;

&lt;p&gt;To follow along with this article, you will need the following things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A DigitalOcean Account (you can sign up for $200 free credits using &lt;a href="https://m.do.co/c/371591aa3027" rel="noopener noreferrer"&gt;this link&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Some basic Linux command-line knowledge (check &lt;a href="https://www.digitalocean.com/community/tutorial-series/getting-started-with-linux" rel="noopener noreferrer"&gt;these articles&lt;/a&gt; for an introduction)&lt;/li&gt;
&lt;li&gt;Basic programming knowledge and familiarity with a coding agent for the Ralph loop use case.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Programmatically Creating DigitalOcean Droplets
&lt;/h1&gt;

&lt;p&gt;A Droplet is a Virtual Private Server (a Linux virtual machine in the cloud) which can be configured with a desired amount of (virtual) CPUs, memory, and storage. Droplets can be provisioned and deprovisioned as we need, making them perfect for creating ephemeral sandbox machines. Additionally, you save costs with &lt;a href="https://www.digitalocean.com/blog/dropletplans-persecbilling-byoip-natgateway" rel="noopener noreferrer"&gt;per-second droplet pricing&lt;/a&gt; that was rolled out recently, since you pay down to the exact second for our droplet usage, rather than an hourly or monthly rate.&lt;/p&gt;

&lt;p&gt;The DigitalOcean API allows you to create and destroy Droplets &lt;em&gt;programmatically&lt;/em&gt;. You can do this by calling &lt;a href="https://docs.digitalocean.com/reference/api/digitalocean/" rel="noopener noreferrer"&gt;API endpoints&lt;/a&gt; from an HTTP client, use one of &lt;a href="https://docs.digitalocean.com/reference/libraries/" rel="noopener noreferrer"&gt;their client libraries&lt;/a&gt;, or the &lt;a href="https://docs.digitalocean.com/reference/doctl/how-to/install/" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;. You will need an API Key to authenticate to the API, which you can create in the &lt;a href="https://docs.digitalocean.com/reference/doctl/how-to/install/" rel="noopener noreferrer"&gt;console&lt;/a&gt;. Create an API Key with at least the &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, and &lt;code&gt;update&lt;/code&gt; permissions for Droplets (you may need to add other permissions if you plan on using other features of DigitalOcean like VPCs and volumes).&lt;/p&gt;

&lt;p&gt;Save this token to your environment so you can use it in the deployment commands later:&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;export &lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_api_token_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use the &lt;code&gt;doctl&lt;/code&gt; CLI or the DigitalOcean API to create a droplet with a provided image and size. You can select one of the base images or a one-click image from the Market, but what if you need something specific that isn't available in these images, such as wanting the sandbox to have specific packages or expose some specific services (like an &lt;a href="https://a2a-protocol.org/latest/topics/what-is-a2a/" rel="noopener noreferrer"&gt;A2A&lt;/a&gt; server).&lt;/p&gt;

&lt;p&gt;DigitalOcean lets you specify a shell script to run when the droplet is created, or write a more sophisticated configuration using &lt;a href="https://cloud-init.io" rel="noopener noreferrer"&gt;cloud-init yaml files&lt;/a&gt;. They provide a nice alternative to custom images in that they don't cost money to store, don't have to be built in advance, but result in longer ready-times for the sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving Beyond Standard Images
&lt;/h2&gt;

&lt;p&gt;While you can easily deploy base images (like Debian or Ubuntu) or Marketplace apps, out-of-the-box templates often fall short if your sandbox needs specific pre-installed packages or custom background services right from boot.&lt;/p&gt;

&lt;p&gt;You could pass a standard bash script to run on startup, but shell scripts can be brittle. A single missing dependency or typo can leave the system in a half-broken state. A more robust alternative is &lt;code&gt;cloud-init&lt;/code&gt;. This industry-standard tool uses structured YAML configurations (called cloud-configs) to handle system setup predictably. They provide a lightweight alternative to building custom machine images in advance, avoiding storage costs while keeping your infrastructure defined as code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the Sandbox Configuration
&lt;/h2&gt;

&lt;p&gt;Let's define a sandbox that installs the latest version of Bun, downloads a web app, exposes an HTTP service, and locks down the firewall. &lt;/p&gt;

&lt;p&gt;Create a file named &lt;code&gt;cloud-config&lt;/code&gt; and add the following YAML. Notice how the configuration is broken down into declarative blocks: managing user access (&lt;code&gt;users&lt;/code&gt;), installing system packages (&lt;code&gt;packages&lt;/code&gt;), writing a systemd daemon directly to the filesystem (&lt;code&gt;write_files&lt;/code&gt;), and finally executing the deployment commands (&lt;code&gt;runcmd&lt;/code&gt;).&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="c1"&gt;#cloud-config&lt;/span&gt;

&lt;span class="c1"&gt;# create a non-root user&lt;/span&gt;
&lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
    &lt;span class="c1"&gt;# the user's real name&lt;/span&gt;
    &lt;span class="na"&gt;gecos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User&lt;/span&gt;
    &lt;span class="c1"&gt;# set their shell to bash&lt;/span&gt;
    &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
    &lt;span class="c1"&gt;# give them sudo privs&lt;/span&gt;
    &lt;span class="na"&gt;sudo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALL=(ALL)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NOPASSWD:ALL"&lt;/span&gt;
    &lt;span class="c1"&gt;# remove password login, we can only log in via ssh keys&lt;/span&gt;
    &lt;span class="na"&gt;lock_passwd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="c1"&gt;# set one or more SSH keys for this user&lt;/span&gt;
    &lt;span class="na"&gt;ssh_authorized_keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# put any ssh keys you want on the user here&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ssh-ed25519 AAAASOME_FINGERPRINT&lt;/span&gt;

&lt;span class="c1"&gt;# run apt-get update (or equivalent on non-Debian systems)&lt;/span&gt;
&lt;span class="na"&gt;package_update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# install curl and unzip (required to install bun)&lt;/span&gt;
&lt;span class="c1"&gt;# ufw is the firewall&lt;/span&gt;
&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unzip&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ufw&lt;/span&gt;

&lt;span class="c1"&gt;# write arbitrary files&lt;/span&gt;
&lt;span class="na"&gt;write_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# create systemd service&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/systemd/system/app.service&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;[Unit]&lt;/span&gt;
      &lt;span class="s"&gt;Description=A simple server&lt;/span&gt;
      &lt;span class="s"&gt;After=network.target&lt;/span&gt;

      &lt;span class="s"&gt;[Service]&lt;/span&gt;
      &lt;span class="s"&gt;Type=simple&lt;/span&gt;
      &lt;span class="s"&gt;User=user&lt;/span&gt;
      &lt;span class="s"&gt;WorkingDirectory=/home/user&lt;/span&gt;
      &lt;span class="s"&gt;ExecStart=/opt/bun/bun run /home/user/index.ts&lt;/span&gt;
      &lt;span class="s"&gt;Restart=always&lt;/span&gt;
      &lt;span class="s"&gt;Environment=PORT=5000&lt;/span&gt;

      &lt;span class="s"&gt;[Install]&lt;/span&gt;
      &lt;span class="s"&gt;WantedBy=multi-user.target&lt;/span&gt;

&lt;span class="c1"&gt;# running arbitrary commands&lt;/span&gt;
&lt;span class="na"&gt;runcmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# download bun&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p /opt/bun&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -fsSLo /opt/bun/bun.zip [https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64.zip](https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64.zip)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unzip -oqd /opt/bun /opt/bun/bun.zip&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mv /opt/bun/bun-linux-x64/bun /opt/bun&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;chmod +x /opt/bun/bun&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rm -r /opt/bun/bun-linux-x64 /opt/bun/bun.zip&lt;/span&gt;
  &lt;span class="c1"&gt;# download the source code&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -fsSLo /home/user/index.ts [https://gist.github.com/arnu515/8c639949ee1a5d226312873151ca40f9/raw/49eeeba4344110d297ed8f30d32ba8f307af4db2/index.ts](https://gist.github.com/arnu515/8c639949ee1a5d226312873151ca40f9/raw/49eeeba4344110d297ed8f30d32ba8f307af4db2/index.ts)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;chown user:user /home/user/index.ts&lt;/span&gt;
  &lt;span class="c1"&gt;# configure the firewall&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ufw allow ssh&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ufw allow 5000/tcp&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ufw enable&lt;/span&gt;
  &lt;span class="c1"&gt;# enable and start the systemd service&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;systemctl enable --now app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Do not remove the &lt;code&gt;#cloud-config&lt;/code&gt; comment at the top of the file, as DigitalOcean requires it to parse the payload. Remember to replace &lt;code&gt;ssh-ed25519 AAAASOME_FINGERPRINT&lt;/code&gt; with your actual public key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Provisioning the Droplet
&lt;/h2&gt;

&lt;p&gt;With the configuration defined, you can deploy the machine. You can do this via the official &lt;code&gt;doctl&lt;/code&gt; CLI or by sending the payload directly to the API using &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you have &lt;code&gt;doctl&lt;/code&gt; installed and authenticated, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute droplet create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-ipv6&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; debian-13-x64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--size&lt;/span&gt; s-1vcpu-512mb-10gb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-data-file&lt;/span&gt; cloud-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; YOUR_REGION_CHOICE &lt;span class="se"&gt;\&lt;/span&gt;
  sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, if you are relying strictly on the API, you can use &lt;code&gt;jq&lt;/code&gt; to properly escape the YAML file's quotes and newlines before piping it into &lt;code&gt;curl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; region &lt;span class="s2"&gt;"YOUR_REGION_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; ud &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;cloud-config&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{
    name: "sandbox",
    region: $region,
    size: "s-1vcpu-512mb-10gb",
    image: "debian-13-x64",
    ipv6: true,
    user_data: $ud
  }'&lt;/span&gt; | curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"[https://api.digitalocean.com/v2/droplets](https://api.digitalocean.com/v2/droplets)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;To find the exact slug for your preferred data center, run &lt;code&gt;doctl compute regions list&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Monitoring and Testing
&lt;/h2&gt;

&lt;p&gt;The underlying droplet infrastructure takes a few seconds to spin up. You can check the provisioning status by listing your droplets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute droplet list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status will initially show as &lt;code&gt;new&lt;/code&gt;. After a brief moment, DigitalOcean will allocate the IP addresses and the status will change to &lt;code&gt;active&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ID           Name        Public IPv4  ...  Status  ...
SOME_NUMBER  sandbox     SOME_IP      ...  active  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the droplet is active, &lt;code&gt;cloud-init&lt;/code&gt; begins executing your configuration in the background. After the system finishes pulling the packages and starting the systemd service, your HTTP server will be live. &lt;/p&gt;

&lt;p&gt;Navigate to &lt;code&gt;http://SOME_IP:5000&lt;/code&gt; in your browser to view your live view counter app. You now have a fully reproducible sandbox environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning Up
&lt;/h2&gt;

&lt;p&gt;Because Droplets are billed by the second, you should destroy the sandbox as soon as you are done experimenting. You can delete it using either of the following methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute droplet delete sandbox

&lt;span class="c"&gt;# or via the API:&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="s2"&gt;"[https://api.digitalocean.com/v2/droplets/DROPLET_ID](https://api.digitalocean.com/v2/droplets/DROPLET_ID)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Why Not Containers?
&lt;/h1&gt;

&lt;p&gt;While running OCI containers locally provide a reasonably sandboxed and reproducible environment, they do still run on your local machine and come with downsides because of that, such as not being able to spin up long running tasks, requiring compute/storage resources, requiring additional software (or possibly hypervisors) installed, etc.&lt;/p&gt;

&lt;p&gt;Running these sandboxes on a VPS provider like DigitalOcean gives you server-grade hardware and networking while being able to have long-running tasks without keeping your local machine up or using its resources.&lt;/p&gt;

&lt;h1&gt;
  
  
  Comparison with Other Sandbox Services
&lt;/h1&gt;

&lt;p&gt;There are services like Daytona or sprites.dev which specifically provide preconfigured sandbox instances complete with client libraries and instant configuration. These services usually work by running a container or micro-VM on a server with a preconfigured image, and charging you per-second of compute you use. These services are more convenient, and containers/micro-VMs are faster to start up, but these services tend to cost much more and have higher vendor lock-in compared to using a VPS provider like DigitalOcean. For example, as of the time of writing, running a 2vCPU + 4GiB RAM sandbox with 10GiB of storage for two hours on Daytona costs $0.33, while the same sandbox running for the same time costs &lt;code&gt;$0.036&lt;/code&gt; on DigitalOcean — almost a 10x difference!&lt;/p&gt;

&lt;p&gt;If you prioritize spin-up speed over cost, these services may be better than using a cloud provider. There also are plenty of sandbox orchestrating projects on GitHub that you can host on a VPS to create sandboxes within that VPS or a fleet of VPSes, but this article will not be covering that. Let me know if you're interested in such a thing and I'll be sure to cover it!&lt;/p&gt;

&lt;p&gt;Unfortunately, there isn't a user-friendly library/command line tool for using DigitalOcean VPSes as sandboxes yet, so you'll have to write scripts that wrap over &lt;a href="https://docs.digitalocean.com/reference/doctl/how-to/install/" rel="noopener noreferrer"&gt;&lt;code&gt;doctl&lt;/code&gt;&lt;/a&gt; or use the &lt;a href="https://docs.digitalocean.com/reference/libraries/" rel="noopener noreferrer"&gt;HTTP API&lt;/a&gt; (optionally through a &lt;a href="https://docs.digitalocean.com/reference/api/digitalocean/" rel="noopener noreferrer"&gt;client library&lt;/a&gt;) instead. This article will henceforth cover using the &lt;code&gt;doctl&lt;/code&gt; command since that's easiest to wrap using something like bash scripts, but these commands are easily translatable to API calls or client library methods.&lt;/p&gt;

&lt;h1&gt;
  
  
  Using Prebuilt Images
&lt;/h1&gt;

&lt;p&gt;Writing a cloud-config and waiting for it to run every time a sandbox is created is cumbersome. It would be better if a droplet could be created from an image that is already configured according to your needs. DigitalOcean supports uploading custom images (billed at $0.06 per GB per month to store) from which Droplets can be created.&lt;/p&gt;

&lt;p&gt;Let's create an image that has a Python interpreter, some JavaScript runtimes, and sane security defaults. This image will be used later in this article as a code execution sandbox. These images &lt;a href="https://docs.digitalocean.com/products/custom-images/how-to/upload/" rel="noopener noreferrer"&gt;must be&lt;/a&gt; Unix-like images with a proper filesystem (ext3/ext4), with cloud-init and sshd installed. These images can be raw (&lt;code&gt;.img&lt;/code&gt;) or in a virtual machine image format like &lt;code&gt;qcow2&lt;/code&gt; or &lt;code&gt;vdi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To create such an image, the easiest way is to create a droplet, make your preferred changes to it, and then create a snapshot of it from the console. You can also create images from QEMU/VirtualBox VMs, or use tools like Packer. You could also create a NixOS configuration and make it output a qcow2 image — for maximal reproducibility.&lt;/p&gt;

&lt;p&gt;This article will cover the first method. Create a droplet and SSH into it. Then, make any desired changes to the droplet. I chose to update the system, create a new user, give it sudo perms, add an SSH key to it, disable root ssh login, set up &lt;code&gt;ufw&lt;/code&gt;, set up &lt;code&gt;fail2ban&lt;/code&gt;, and finally, install python3, nodejs, and bun. Feel free to follow these steps, or change them as you wish.&lt;/p&gt;

&lt;p&gt;Once your droplet is ready, create a snapshot of it to save it as an image: (these steps can also be done from the DigitalOcean console)&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="c"&gt;# power off the droplet  &lt;/span&gt;
doctl compute droplet-action power-off DROPLET_ID

&lt;span class="c"&gt;# create a snapshot — note the region  &lt;/span&gt;
doctl compute droplet-action snapshot &lt;span class="nt"&gt;--snapshot-name&lt;/span&gt; &lt;span class="s1"&gt;'sandbox-image'&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; DROPLET_ID

&lt;span class="c"&gt;# delete the droplet  &lt;/span&gt;
doctl compute droplet delete DROPLET_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can use this image to create a droplet. You can only create the droplet in the region you created the snapshot (i.e. the region of the earlier droplet). You can add snapshots (and user-created images) to other regions at no additional cost from the console.&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="c"&gt;# note the image id of the desired image  &lt;/span&gt;
doctl compute image list-user

doctl compute droplet create &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image&lt;/span&gt; IMAGE_ID &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--size&lt;/span&gt; s-1vcpu-1gb &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; REGION &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--sandbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feel free to scour the DigitalOcean one-click app marketplace or for community created images on the web for readymade images you can use.&lt;/p&gt;

&lt;h1&gt;
  
  
  Persist Sandbox Contents
&lt;/h1&gt;

&lt;p&gt;There are some situations in which you don't want an ephemeral sandbox, but you need compute that can be spun up and down on demand while keeping data changes. A common example of this is something like GitHub codespaces, where you want to persist local code changes the user has made, but spin down the compute when they're done editing. They should then be able to get back to editing from the point they left off later.&lt;/p&gt;

&lt;p&gt;DigitalOcean allows you to attach a (network-attached) storage pool that persists over droplet destruction. They're called block storage volumes. They can be created and attached to a droplet to provide it with storage that doesn't get destroyed with the droplet. Do keep in mind that volumes can only be attached to Droplets in the same region.&lt;/p&gt;

&lt;p&gt;If you're looking for storage to upload, say build artifacts/output, or other such output files that aren't read from once they're written to, need to be accessed from tooling outside the sandbox, or do not need to be available across sandboxes, but need to be available somewhere, then I recommend using object storage (like DigitalOcean Spaces) instead. This would mean having to write some additional software that your sandbox will have to execute to upload these files to the object storage (it could be as simple as a cURL script). Block storage volumes are recommended in use cases requiring constant access and modification of created files, rather than "upload and forget".&lt;/p&gt;

&lt;p&gt;The below command creates a 5GiB volume: (be sure to note the volume's ID)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute volume create &lt;span class="nt"&gt;--region&lt;/span&gt; blr1 &lt;span class="nt"&gt;--size&lt;/span&gt; 5GiB &lt;span class="nt"&gt;--fs-type&lt;/span&gt; ext4 &lt;span class="nt"&gt;--fs-label&lt;/span&gt; storage sandbox-volume
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The volume can be attached to a running droplet like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute volume-action attach VOLUME_ID DROPLET_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the action completes, the volume is available on the droplet at &lt;code&gt;/mnt/sandbox_volume&lt;/code&gt;. Writing to any files in this volume will ensure that those files are persisted even after the droplet is destroyed. You can remove a volume from a droplet by using the &lt;code&gt;detach&lt;/code&gt; subcommand of &lt;code&gt;volume-action&lt;/code&gt; in &lt;code&gt;doctl&lt;/code&gt;. If you delete this droplet now, the volume will still remain (but it'll be detached). You are still charged for the volume, even if it isn't attached to anything.&lt;/p&gt;

&lt;p&gt;When creating a new droplet, you can directly attach the volume to it using the &lt;code&gt;--volumes&lt;/code&gt; flag with the &lt;code&gt;droplet create&lt;/code&gt; subcommand. The volume will be attached to the same place and the files that were created earlier will be available. Unlike object storage, the files on the volume cannot be accessed from outside Droplets.&lt;/p&gt;

&lt;p&gt;Volumes can only be attached to one droplet at a time (to prevent from data corruption due to race conditions). If you want multiple Droplets to access data from the same volume at the same time, you'll have to use a network filesystem hosted on one of the Droplets with the volume attached.&lt;/p&gt;

&lt;h1&gt;
  
  
  Attaching a Fixed IP to the Sandbox
&lt;/h1&gt;

&lt;p&gt;Up until now, all the Droplets you've created have had different IPs. This isn't really useful since you'd want to reach your sandboxes over the internet to use them. DigitalOcean allows you to reserve IPv4 and IPv6 addresses that you can attach to Droplets to give them a deterministic address that they can be reached at.&lt;/p&gt;

&lt;p&gt;IPv4 reserved addresses cost money — even if they're not attached to a droplet, but IPv6 addresses are free to reserve. If your network supports IPv6, you can get away with not reserving IPv4 addresses and only using IPv6 to communicate with your Droplets.&lt;/p&gt;

&lt;p&gt;Create a fixed IP through the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute reserved-ip create &lt;span class="nt"&gt;--region&lt;/span&gt; REGION  
doctl compute reserved-ipv6 create &lt;span class="nt"&gt;--region&lt;/span&gt; REGION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then attach a reserved IP to an existing droplet using the following command: (the IP and droplet must be in the same region)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute reserved-ip-action attach IPV4_ADDR DROPLET_ID  
doctl compute reserved-ipv6 attach IPV6_ADDR DROPLET_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, there's no way to create a droplet and assign it a reserved IP immediately. You'll have to run the above command to assign it the IP after creation manually. Alternatively, you could fetch the droplet's IP address by using the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doctl compute droplet get &lt;span class="nt"&gt;--template&lt;/span&gt; &lt;span class="s1"&gt;'{{.PublicIPv4}}'&lt;/span&gt; sandbox  
doctl compute droplet get &lt;span class="nt"&gt;--template&lt;/span&gt; &lt;span class="s1"&gt;'{{.PublicIPv6}}'&lt;/span&gt; sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may want to take a look at the &lt;a href="https://docs.digitalocean.com/products/networking/reserved-ips/getting-started/" rel="noopener noreferrer"&gt;Reserved IP documentation&lt;/a&gt; if you want to do some further configuration, like making the droplet send outbound traffic through the reserved IP. Some of these changes may require the &lt;code&gt;ip&lt;/code&gt; command, which you can persist across sandboxes either by updating the image, or by writing a specific cloud-config (I recommend the latter since if your reserved IP changes in the future, that would mean updating the image with the former method).&lt;/p&gt;

&lt;h1&gt;
  
  
  Use-case: A Ralph loop Agent
&lt;/h1&gt;

&lt;p&gt;If you're familiar with LLM-assisted programming (using large language models to automate and enhance software development tasks), you must have heard of a Ralph loop, which, in its simplest state, is the following bash loop:&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="k"&gt;while&lt;/span&gt;:&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;prompt.md | CODING_AGENT &lt;span class="nt"&gt;--allow-all-perms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What is a Ralph loop?&lt;/strong&gt; It's an agent that is fed a prompt from a file over and over again until it performs some big task. At every iteration of the agent, it can edit the prompt file to give instructions to the next iteration of the agent that runs. Ralph loops can either produce bug-free complicated software due to its many iterations and precise instructions, or end up going in a death spiral after one of its iterations makes a mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why sandbox a Ralph loop?&lt;/strong&gt; This is the perfect use-case for a sandbox. When a Ralph loop goes rogue and starts breaking apart, you should be able to contain the fallout. There are many cases of agents going rogue and deleting files, writing bad code, running arbitrary commands, etc., and stuff like that should not be happening on a machine you care about. A sandbox is ideal for running Ralph loops, especially in unsupervised cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to set up the sandbox?&lt;/strong&gt; 1. &lt;strong&gt;Base Image:&lt;/strong&gt; You'd start by using a base image with all of your necessary tools included — such as programming languages, build tools, and an LLM coding tool.   &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;cloud-config:&lt;/strong&gt; Then, you'd add a cloud-config that creates the initial &lt;code&gt;prompt.md&lt;/code&gt; file, clones the repo, sets up credentials &amp;amp; API keys, etc. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Things to consider&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The repo should be in a block volume since if the agent goes rogue, you'll still have the repo that you can restore using &lt;code&gt;git&lt;/code&gt; after deleting the sandbox. The original Ralph loop article linked above recommends providing detailed specifications and making the agent only do one task per loop. &lt;/p&gt;

&lt;p&gt;Make sure your Ralph loop knows how to exit (a common way is to exit if the previous iteration's prompt is the same as the next — but make this clear in the system prompt of the agent).&lt;/p&gt;

&lt;p&gt;You should also tell the agent to periodically commit its work using &lt;code&gt;git&lt;/code&gt;. You'll need to upload the results (if not using a volume) and delete the sandbox after the agent is done running. You can check if the agent is still active by periodically using the &lt;code&gt;ps&lt;/code&gt; command to check for your coding tool's process. It is also a good practice to have a cronjob on your main machine to delete these Ralph sandboxes in case the harness crashes or the agent takes too long to save on compute and tokens.&lt;/p&gt;

&lt;p&gt;You'll need to write a harness that spins up these sandboxes with a cloud-config that creates the initial prompt.md and any other supporting files, waits for the agent to finish executing, and then spins down the sandbox. It could also perform some additional steps, like creating a PR from the resulting code, or running a code review, or something else. This harness should be extremely specific to your work, due to the non-deterministic nature of LLMs. A generic harness may make the LLM go rogue more often, and thus, I leave the harness creation to you. Do tell about your experience in the comments!&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQs
&lt;/h1&gt;

&lt;h3&gt;
  
  
  What happens to data stored on a sandbox Droplet when it is destroyed?
&lt;/h3&gt;

&lt;p&gt;Because Droplets act as completely isolated virtual machine instances, destroying a Droplet permanently deletes its root filesystem and any local ephemeral storage. To preserve logs, source trees, or runtime artifacts, you must connect a persistent network attached component like a &lt;strong&gt;DigitalOcean Block Storage Volume&lt;/strong&gt; or upload output artifacts directly to &lt;strong&gt;DigitalOcean Spaces&lt;/strong&gt; object storage prior to executing the destruction command.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does DigitalOcean’s per-second billing benefit automated AI workflows?
&lt;/h3&gt;

&lt;p&gt;Traditional cloud VPS platforms bill on a strict hourly minimum window even if an instance only runs for a few minutes. DigitalOcean calculates compute usage down to the exact second. For autonomous AI testing workflows—where a harness might spin up an agent, run a suite of tests in two minutes, and instantly destroy the environment—you only pay for those exact 120 seconds of resource usage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I attach a single DigitalOcean Volume to multiple sandbox Droplets simultaneously?
&lt;/h3&gt;

&lt;p&gt;No. DigitalOcean Block Storage Volumes use standard raw block mappings (such as &lt;code&gt;ext4&lt;/code&gt;) that are not designed to safely negotiate concurrent write operations across multiple independent operating systems. Attaching a single volume to multiple systems simultaneously can cause catastrophic filesystem corruption. For multi-agent configurations requiring shared access, configure a Network File System (NFS) server wrapper on a host Droplet instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why should I choose cloud-init over standard custom snapshots for my sandbox environments?
&lt;/h3&gt;

&lt;p&gt;While custom machine images provide faster initial boot sequences, they require maintenance overhead, incur minor storage costs ($0.06/GB per month), and must be regenerated whenever global dependency targets shift. &lt;code&gt;cloud-init&lt;/code&gt; allows you to manage sandbox setups entirely via infrastructure-as-code configurations, modifying system behaviors dynamically on the fly without changing baseline snapshots.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;To combat the unpredictability of agentic-AI systems, they must be run on disposable machines, while still being able to preserve certain changes and outputs of those agents. Sandboxes fill this criterion quite well, not only for agentic applications but also in use cases where quick ephemeral compute is required. DigitalOcean provides raw compute for cheap, and using the techniques prescribed in this article, you can use these resources as a sandboxing tool. Their products are cost-effective and provide more control over using specific sandboxing services, but they may not be DX-optimized or be the fastest to start up. You can reach a middle ground by using a self-hostable sandbox orchestrator over one or multiple Droplets that uses either containers or micro-VMs (Firecracker). Let me know if you want to see more articles about this topic!&lt;/p&gt;

&lt;p&gt;I can't wait to see all the things you create with the ideas mentioned in this article! Please tell me about your experiences or queries, if you have any, in the comments below!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of the &lt;a href="https://www.digitalocean.com/solutions/digitalocean-ripple-writers-program" rel="noopener noreferrer"&gt;DigitalOcean Ripple Writers program&lt;/a&gt;. I received compensation and platform credits for writing this content, but all technical assessments, code, and opinions are my own based on hands-on testing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>agents</category>
    </item>
    <item>
      <title>SupportAddress — An email-first support platform</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Mon, 09 Jun 2025 00:04:09 +0000</pubDate>
      <link>https://dev.to/arnu515/supportaddress-an-email-first-support-platform-1lo5</link>
      <guid>https://dev.to/arnu515/supportaddress-an-email-first-support-platform-1lo5</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/postmark"&gt;Postmark Challenge: Inbox Innovators&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://support.aarnavpai.in" rel="noopener noreferrer"&gt;SupportAddress&lt;/a&gt; is an email-first support platform that lets your customers reach out for support directly through email. There's no learning difficult eccentric software, creating accounts, or checking back at a webpage every now-and-then to see if the support ticket has been updated; all of that is done via email!&lt;/p&gt;

&lt;p&gt;Create subgroups within organisations to automatically sort incoming tickets using AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The application is hosted live at &lt;a href="https://support.aarnavpai.in" rel="noopener noreferrer"&gt;https://support.aarnavpai.in&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Testing instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign in/up at &lt;a href="https://support.aarnavpai.in" rel="noopener noreferrer"&gt;https://support.aarnavpai.in&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If you're signing up, you'll receive an OTP in your email. Enter that, and then choose a display name and password.&lt;/li&gt;
&lt;li&gt;Once logged in, you'll be taken to the dashboard, where you'll be asked to join or create an organisation. Create an organisation.&lt;/li&gt;
&lt;li&gt;Your &lt;em&gt;SupportAddress&lt;/em&gt; will be displayed at your organisation's home page. Give this SupportAddress to your customers for them to email any support queries they may have to it.&lt;/li&gt;
&lt;li&gt;You may create subgroups (from the organisation actions section at the bottom of the organistation home page). Giving a description to your subgroup lets AI attempt to automatically send any matching support tickets to it.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can invite other people to your organisation, and once they've joined, you can add them to subgroups. People who are not part of a subgroup will not be allowed to access its tickets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To create a ticket, email the organisation's SupportAddress, or the SupportAddress of any individual subgroup. The tickets list should update automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Support Agents can assign themselves to a ticket. If they do that, nobody else will be able to reply to or close that ticket, unless that agent unassigns themselves. Replying to a ticket will send an email to the customer with the reply.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once a ticket is closed, it can only be re-opened if the customer who opened the ticket replies to the ticket mail thread.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Screenshots:&lt;/p&gt;

&lt;p&gt;Join/Create organisation page:&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%2Fng6ph034flm5h27f8p02.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%2Fng6ph034flm5h27f8p02.png" alt="The join/create organisation page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Subgroup creation page:&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%2F946vyyw5yvt3u13hnz6l.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%2F946vyyw5yvt3u13hnz6l.png" alt="Subgroup creation page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Org Home page:&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%2Frk9u6gc5efxry94wp8ot.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%2Frk9u6gc5efxry94wp8ot.png" alt="Organisation Home Page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sending an email to the SupportAddress:&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%2F5o7o8xjgt0u1cn69cn43.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%2F5o7o8xjgt0u1cn69cn43.png" alt="Sending an email to the SupportAddress"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A new ticket was automatically created and categorised:&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%2Fdtlind9cdroohth2371e.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%2Fdtlind9cdroohth2371e.png" alt="The newly created ticket page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Send a reply from the dashboard:&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%2Fzrealjah9ye9dbvzideh.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%2Fzrealjah9ye9dbvzideh.png" alt="Reply form"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With attachment support:&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%2F836e25s3675fxq4ktbrg.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%2F836e25s3675fxq4ktbrg.png" alt="Attachment support"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Repository
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/arnu515" rel="noopener noreferrer"&gt;
        arnu515
      &lt;/a&gt; / &lt;a href="https://github.com/arnu515/supportaddress" rel="noopener noreferrer"&gt;
        supportaddress
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Email-first customer support
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;SupportAddress&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;The email-first support platform that lets your customers reach out for support directly through email.&lt;/p&gt;
&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/postmark" rel="nofollow"&gt;Postmark Challenge: Inbox Innovators&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is hosted live at &lt;a href="https://support.aarnavpai.in" rel="nofollow noopener noreferrer"&gt;https://support.aarnavpai.in&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Read more about this project &lt;a href="https://dev.to/arnu515/supportaddress-an-email-first-support-platform-1lo5" rel="nofollow"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/arnu515/supportaddress" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://qwik.dev" rel="noopener noreferrer"&gt;QwikJS&lt;/a&gt; for this project. Qwik is a relatively new JavaScript framework that uses Resumability for client-side interactivity rather than Hydration. Resumability means that the client-side bundle can essentially "pick-up" where the server-side rendering stopped, i.e. adding event listeners, running on-mount hooks, etc., instead of having to re-render the entire app from scratch like how traditional SSR frameworks do (Next, SvelteKit, etc.). I used QwikJS before for &lt;a href="https://aarnavpai.in" rel="noopener noreferrer"&gt;my website&lt;/a&gt;, but that website is mostly static content, so to give Qwik a real try, I decided to use it for this project.&lt;/p&gt;

&lt;p&gt;Other components of the tech stack include Supabase for the database, auth, storage, and functions; TailwindCSS for the styling; Cloudflare Workers AI for the AI models; and Vercel for deployment. A full list of used libraries can be found in the &lt;code&gt;package.json&lt;/code&gt; file in the repository.&lt;/p&gt;

&lt;p&gt;Postmark was used for sending and receiving emails. When a customer emails the SupportAddress, Postmark calls a Supabase edge function with the email data. That edge function sanitizes the email, determines whether it belongs to an existing ticket, or creates a new ticket, and uses AI to categorize it into a subcategory. Any attachments received are uploaded to Supabase storage by the function. Finally, if the ticket is closed, the function opens it.&lt;/p&gt;

&lt;p&gt;When a Support Agent replies to, or closes a ticket, an email is sent to the customer using Postmark's API. The API makes it very easy to send emails, including with attachments and custom SMTP headers, without having to set up an entire SMTP connection.&lt;/p&gt;

&lt;p&gt;I'd like to thank Postmark for giving me an opportunity to explore email parsing, QwikJS, and AI models! I wanted to use more AI in this project, but unfortunately due to me starting this challenge much later than I was supposed to, I wasn't able to add more AI features. Thanks for stopping by!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>postmarkchallenge</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>I wanna Do ____! — An AI powered habit building application</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Mon, 20 Jan 2025 00:07:46 +0000</pubDate>
      <link>https://dev.to/arnu515/i-wanna-do-an-ai-powered-habit-building-application-3kpi</link>
      <guid>https://dev.to/arnu515/i-wanna-do-an-ai-powered-habit-building-application-3kpi</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github"&gt;GitHub Copilot Challenge &lt;/a&gt;: New Beginnings&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A Habit tracking application that uses AI to create tasks and goals for you which help you build good habits!&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The live demo is hosted at: &lt;a href="https://do.aarnavpai.in" rel="noopener noreferrer"&gt;https://do.aarnavpai.in&lt;/a&gt; &lt;br&gt;
The source code is at: &lt;a href="https://github.com/arnu515/i-wanna-do" rel="noopener noreferrer"&gt;https://github.com/arnu515/i-wanna-do&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screenshots:&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%2Fi2hw4je5w60xfxuaz3lp.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%2Fi2hw4je5w60xfxuaz3lp.png" alt=" " width="800" height="465"&gt;&lt;/a&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%2Fd9f7o2dg0mw31f6149n8.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%2Fd9f7o2dg0mw31f6149n8.png" alt=" " width="800" height="306"&gt;&lt;/a&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%2Fhpvzqq6a7ek0iaojy3t8.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%2Fhpvzqq6a7ek0iaojy3t8.png" alt=" " width="800" height="644"&gt;&lt;/a&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%2F79wp9uckk2e6gui4y1pm.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%2F79wp9uckk2e6gui4y1pm.png" alt=" " width="800" height="644"&gt;&lt;/a&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%2Fh5p8q0r8qv8s3l01dder.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%2Fh5p8q0r8qv8s3l01dder.png" alt=" " width="800" height="639"&gt;&lt;/a&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%2F77avlfxejy8uftthr9bn.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%2F77avlfxejy8uftthr9bn.png" alt=" " width="750" height="1334"&gt;&lt;/a&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%2F3duil913map04pbt7coc.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%2F3duil913map04pbt7coc.png" alt=" " width="750" height="1334"&gt;&lt;/a&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%2Fk69sjf4qljpgkjl3jc2c.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%2Fk69sjf4qljpgkjl3jc2c.png" alt=" " width="750" height="1334"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/arnu515" rel="noopener noreferrer"&gt;
        arnu515
      &lt;/a&gt; / &lt;a href="https://github.com/arnu515/i-wanna-do" rel="noopener noreferrer"&gt;
        i-wanna-do
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      AI powered habit building app!
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;I wanna Do!&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;An app that lets you build habits with AI!&lt;/p&gt;
&lt;p&gt;This application was built for the &lt;a href="https://dev.to/challenges/github" rel="nofollow"&gt;GitHub Copilot 1-Day Build Challenge&lt;/a&gt; (and it was built in ~19h!)&lt;/p&gt;
&lt;p&gt;It is hosted live at &lt;a href="https://do.aarnavpai.in" rel="nofollow noopener noreferrer"&gt;https://do.aarnavpai.in&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For more information, check &lt;a href="https://dev.to/arnu515/i-wanna-do-an-ai-powered-habit-building-application-3kpi" rel="nofollow"&gt;this post on DEV&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/arnu515/i-wanna-do" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Copilot Experience
&lt;/h2&gt;

&lt;p&gt;Copilot was very useful in this task. I'm sure it would have taken me twice as long if it weren't for Copilot. Copilot does repetitive tasks very well, for example, converting an SQL schema into a typescript interface, or converting JSON to typescript interfaces. It can also do all the boilerplate code, so you focus on what matters. Copilot chat is useful for performing tedious refactors that would require command line scripts to do! Copilot chat was also useful in planning this application.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Models
&lt;/h2&gt;

&lt;p&gt;This application uses GitHub Models to plan out a habit building journey. The LLM converts whatever habit the user wishes to build into a proper roadmap with goals and tasks, breaking down big tasks into smaller ones and making them more approachable.&lt;/p&gt;

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

&lt;p&gt;This was a unique challenge because of the time limit. There were so many things I wanted to add but couldn't because of the time constraints. Nevertheless, it was a lot of fun! I hope GitHub comes back with more challenges in the future!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Easy development environments with Nix and Nix flakes!</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Wed, 15 Jan 2025 15:04:37 +0000</pubDate>
      <link>https://dev.to/arnu515/easy-development-environments-with-nix-and-nix-flakes-21mb</link>
      <guid>https://dev.to/arnu515/easy-development-environments-with-nix-and-nix-flakes-21mb</guid>
      <description>&lt;p&gt;In this article, we shall cover declarative development shells with Nix flakes! If you're new to Nix, I recommend checking out the previous two articles in this series to get a better understanding, since this article assumes that you've read the previous two already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's revise: Creating a Shell
&lt;/h2&gt;

&lt;p&gt;The previous article introduced you to the &lt;code&gt;nix shell&lt;/code&gt; command, which downloads/builds packages and puts you in a shell environment with them in the &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You need not run the examples given below, they're just provided for illustration purposes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By default, &lt;code&gt;nix shell&lt;/code&gt; drops you into a shell (which is your login shell) with the packages in &lt;code&gt;$PATH&lt;/code&gt;. You can also specify an arbitrary command to run, for example:&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="c"&gt;# Postgres client&lt;/span&gt;
nix shell nixpkgs#postgres_17 &lt;span class="nt"&gt;--command&lt;/span&gt; psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, you can also use another shell (that should exist in &lt;code&gt;$PATH&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix shell nixpkgs#nushell &lt;span class="nt"&gt;--command&lt;/span&gt; nushell  &lt;span class="c"&gt;# https://www.nushell.sh/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also run multiple commands like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix shell nixpkgs#gnumake &lt;span class="nt"&gt;--command&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"make &amp;amp;&amp;amp; make install"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to run the package itself, you need not use &lt;code&gt;nix shell&lt;/code&gt;, since the &lt;code&gt;nix run&lt;/code&gt; command also exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix run nixpkgs#vim &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--log&lt;/span&gt; some.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are some more things that &lt;code&gt;nix shell&lt;/code&gt; can do, which I will not be covering here, but they are covered &lt;a href="https://nix.dev/manual/nix/2.24/command-ref/new-cli/nix3-env-shell" rel="noopener noreferrer"&gt;in the reference manual&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;nix shell&lt;/code&gt; to create a development environment
&lt;/h3&gt;

&lt;p&gt;This can be done, but it is tedious. Imagine having to type dozens of dependencies manually by hand every time you want to enter your development environment, or even if you put it in a script, or use the &lt;a href="https://nix.dev/manual/nix/2.24/command-ref/new-cli/nix3-env-shell#opt-stdin" rel="noopener noreferrer"&gt;&lt;code&gt;--stdin&lt;/code&gt; argument&lt;/a&gt;, you'll still have the risk of installing a different version of the package than required. You may also like to customize the shell environment a bit more, like populating environment variables, running commands before the shell starts, etc.&lt;/p&gt;

&lt;p&gt;Instead, there's a better way..&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Development Shell with Flakes
&lt;/h2&gt;

&lt;p&gt;Development shells (or shell environments as they're called in non-flake land) are a feature of Nix that allow you to all the things mentioned above — install specific versions of packages, set environment variables, run commands before the shell starts, etc.&lt;/p&gt;

&lt;p&gt;Let's start with the same &lt;code&gt;flake.nix&lt;/code&gt; as last time. You could copy it from &lt;a href="https://gitlab.com/arnu515-tutorials/nix/-/blob/master/a-simple-flake/flake.nix?ref_type=heads" rel="noopener noreferrer"&gt;here&lt;/a&gt;, but I'd like to digress a bit by introducing Nix templates!&lt;/p&gt;

&lt;p&gt;Templates are a way for you to create, well template folders which are declared by a flake. Nix can then copy those files over from a flake into your current directory with the &lt;a href="https://nix.dev/manual/nix/2.24/command-ref/new-cli/nix3-flake-init" rel="noopener noreferrer"&gt;&lt;code&gt;nix flake init&lt;/code&gt;&lt;/a&gt; command. Nix makes templates declarative and versioned too!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Contrary to the command's reference manual, there's no need for the template itself to be a flake!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A template is pretty easy to create, you just need to add an attribute set called &lt;code&gt;templates&lt;/code&gt; in your flake's &lt;code&gt;outputs&lt;/code&gt;. The keys of the attrset are the template names, and the values are an attribute set with three fields: &lt;code&gt;description&lt;/code&gt;, a description of the template; &lt;code&gt;path&lt;/code&gt;, the path to the template folder; and &lt;code&gt;welcomeText&lt;/code&gt;, some text that is output when someone uses the template.&lt;/p&gt;

&lt;p&gt;I've made the simple flake from last article into a template hosted at this &lt;a href="https://github.com/arnu515-tutorials/dotfiles" rel="noopener noreferrer"&gt;article series' GitLab page&lt;/a&gt;, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="sx"&gt;https://gitlab.com/arnu515-tutorials/nix/-/blob/master/flake.nix&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nv"&gt;ref_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;heads&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="nv"&gt;templates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;simple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A simple flake"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;./a-simple-flake&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;welcomeText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;        Welcome to Nix!&lt;/span&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="s2"&gt;        This flake exports a single package, `hello`, which can be&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;        executed by running:&lt;/span&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="s2"&gt;        $ nix run #.hello&lt;/span&gt;&lt;span class="err"&gt;

&lt;/span&gt;&lt;span class="s2"&gt;        Check out my article series on Nix for more information!&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;        https://dev.to/arnu515/my-new-nix-series-2cc3&lt;/span&gt;&lt;span class="err"&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="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;You can use a template by using the &lt;code&gt;nix flake init&lt;/code&gt; command. Beware that &lt;code&gt;nix flake init&lt;/code&gt; will copy the template's files to the current directory, but it will not overwrite existing files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix flake init &lt;span class="nt"&gt;--template&lt;/span&gt; gitlab:arnu515-tutorials/nix#simple 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that Nix checks the fragment &lt;code&gt;simple&lt;/code&gt; (called a flake output name) starting from the &lt;code&gt;templates&lt;/code&gt; attrset, then the rest of the flake, unlike in &lt;code&gt;nix shell/run/build&lt;/code&gt;, which started from the &lt;code&gt;packages&lt;/code&gt; attrset, then moved to the &lt;code&gt;legacyPackages&lt;/code&gt; attrset, then the rest of the flake.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Creating a development shell&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now let's create the development shell. This will be created in the &lt;code&gt;devShells&lt;/code&gt; attrset, which looks just like the &lt;code&gt;packages&lt;/code&gt; attrset, i.e. it has another attrset inside it for each system (like &lt;code&gt;x86_64-linux&lt;/code&gt;), and those attrsets declare development shells inside them.&lt;/p&gt;

&lt;p&gt;Just like &lt;code&gt;packages&lt;/code&gt;, a development shell named &lt;code&gt;default&lt;/code&gt; is the default development shell.&lt;/p&gt;

&lt;p&gt;Here's a simple shell that has the &lt;code&gt;hello&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Nix devshells!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:nixos/nixpkgs/nixpkgs-24.11"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt;
    &lt;span class="nv"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x86_64-linux"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;inherit&lt;/span&gt; &lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kn"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;devShells&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;simple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkShell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;hello&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nv"&gt;shellHook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;        &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;hello&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/hello&lt;/span&gt;&lt;span class="err"&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="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;There's quite a few changes from earlier! First, the &lt;code&gt;hello&lt;/code&gt; package was removed, since we don't need it for this demonstration. Then, two variables called &lt;code&gt;system&lt;/code&gt; and &lt;code&gt;pkgs&lt;/code&gt; were created using the &lt;a href="https://nix.dev/tutorials/nix-language#let-in" rel="noopener noreferrer"&gt;&lt;code&gt;let ... in ...&lt;/code&gt; construct&lt;/a&gt;. Finally, we create a shell using the &lt;a href="https://nixos.org/manual/nixpkgs/stable/#sec-pkgs-mkShell" rel="noopener noreferrer"&gt;&lt;code&gt;pkgs.mkShell&lt;/code&gt;&lt;/a&gt; function, specifying &lt;code&gt;packages&lt;/code&gt; to be put in path, and a &lt;code&gt;shellHook&lt;/code&gt; to run before the shell starts. Let's dissect this one-by-one.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;${...}&lt;/code&gt; syntax does string interpolation, replacing itself with the evaluated string inside the braces. &lt;code&gt;pkgs.hello&lt;/code&gt; evaluates to the nix store path of the &lt;code&gt;hello&lt;/code&gt; package. Thus, we need to append &lt;code&gt;/bin/hello&lt;/code&gt; to actually point to the &lt;code&gt;hello&lt;/code&gt; executable. Note that just &lt;code&gt;hello&lt;/code&gt; could also have been specified, since it will be available in &lt;code&gt;$PATH&lt;/code&gt;, but it is better to specify an absolute path like this so you know where your dependencies are coming from.&lt;/p&gt;

&lt;p&gt;Next, let's talk about what &lt;code&gt;pkgs = import nixpkgs { inherit system; };&lt;/code&gt; does. The &lt;a href="https://nix.dev/tutorials/nix-language#import" rel="noopener noreferrer"&gt;&lt;code&gt;import&lt;/code&gt; function&lt;/a&gt; takes in a path, and evaluates it if it is a path to a Nix file, or evaluates the &lt;code&gt;default.nix&lt;/code&gt; file within the directory if it is a path to a directory (which is the case when a flake reference, like &lt;code&gt;nixpkgs&lt;/code&gt; is passed to &lt;code&gt;import&lt;/code&gt;), and returns the evaluation. Since &lt;code&gt;nixpkgs&lt;/code&gt; points to the &lt;a href="https://github.com/nixos/nixpkgs" rel="noopener noreferrer"&gt;nixpkgs&lt;/a&gt; flake, which is a directory, it evaluates the &lt;code&gt;default.nix&lt;/code&gt; file present within the flake, which in the case of nixpkgs returns a function accepting, among others, an argument for the current &lt;code&gt;system&lt;/code&gt;, which is what we have passed to it. It then returns the set of packages for that system, which is what is bound to &lt;code&gt;pkgs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nixos.org/manual/nixpkgs/stable/#sec-pkgs-mkShell" rel="noopener noreferrer"&gt;&lt;code&gt;pkgs.mkShell&lt;/code&gt;&lt;/a&gt; function creates a development shell / shell environment for us. A shell environment is actually just another derivation (that's why it is possible to run &lt;code&gt;nix develop&lt;/code&gt; on packages itself). In fact, &lt;code&gt;pkgs.mkShell&lt;/code&gt; is just a wrapper around &lt;code&gt;stdenv.mkDerivation&lt;/code&gt;, with some conveniences like specifying &lt;code&gt;packages&lt;/code&gt; instead of &lt;code&gt;nativeBuildInputs&lt;/code&gt;, and concatenating &lt;code&gt;shellHook&lt;/code&gt;s from all inputs, as visible in its &lt;a href="https://github.com/NixOS/nixpkgs/blob/nixos-24.11/pkgs/build-support/mkshell/default.nix" rel="noopener noreferrer"&gt;source code&lt;/a&gt;. There also exists a &lt;code&gt;pkgs.mkShellNoCC&lt;/code&gt;, which does the same as &lt;code&gt;pkgs.mkShell&lt;/code&gt;, but does not introduce a C compiler in the shell. This is useful if you're developing projects that do not need a C compiler installed.&lt;/p&gt;

&lt;p&gt;Let's enter this shell by running:&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="nv"&gt;$ &lt;/span&gt;nix develop .#simple
Hello, world!
&lt;span class="o"&gt;(&lt;/span&gt;nix:nix-shell-env&lt;span class="o"&gt;)&lt;/span&gt; bash-5.2&lt;span class="nv"&gt;$ &lt;/span&gt;which cc  &lt;span class="c"&gt;# a C compiler was added to thes shell by Nix&lt;/span&gt;
/nix/store/888bkaqdpfpx72dd8bdc69qsqlgbhcvf-gcc-wrapper-13.3.0/bin/cc
&lt;span class="o"&gt;(&lt;/span&gt;nix:nix-shell-env&lt;span class="o"&gt;)&lt;/span&gt; bash-5.2&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;which cc  &lt;span class="c"&gt;# different C compiler!&lt;/span&gt;
/usr/bin/cc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;With &lt;code&gt;nix develop&lt;/code&gt;, the flake output name is searched starting from &lt;code&gt;devShells&lt;/code&gt;, then &lt;code&gt;packages&lt;/code&gt;, then &lt;code&gt;legacyPackages&lt;/code&gt;, and finally the whole flake.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;An actual development environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The earlier examples were really simple! Let's create an actual development environment with a &lt;em&gt;better&lt;/em&gt; shell, a NodeJS install, pnpm, and even a redis database!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Nix devshells!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:nixos/nixpkgs/nixos-24.11"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt;
    &lt;span class="nv"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x86_64-linux"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;inherit&lt;/span&gt; &lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c"&gt;# A custom config for valkey&lt;/span&gt;
    &lt;span class="c"&gt;# Declative configs!&lt;/span&gt;
    &lt;span class="nv"&gt;valkeyConfStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;requirepass super-strong-password&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;port 12345&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    ''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c"&gt;# This special package writes the valkey configuration to&lt;/span&gt;
    &lt;span class="c"&gt;# a text file. For more such packages, see:&lt;/span&gt;
    &lt;span class="c"&gt;# https://nixos.org/manual/nixpkgs/stable/#trivial-builder-writeTextFile&lt;/span&gt;
    &lt;span class="c"&gt;#&lt;/span&gt;
    &lt;span class="c"&gt;# The package outputs the path to the file&lt;/span&gt;
    &lt;span class="nv"&gt;valkeyConf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;writeTextFile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"valkey.conf"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;valkeyConfStr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kn"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;devShells&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;simple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkShell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;hello&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;shellHook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;hello&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/hello&lt;/span&gt;&lt;span class="err"&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="nv"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkShell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="c"&gt;# newer versions of redis are not packaged in&lt;/span&gt;
          &lt;span class="c"&gt;# nixpkgs, so we're using valkey, an open-source&lt;/span&gt;
          &lt;span class="c"&gt;# fork of redis maintained by the Linux Foundation&lt;/span&gt;
          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;valkey&lt;/span&gt;

          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nodejs_22&lt;/span&gt;
          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nodePackages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;pnpm&lt;/span&gt;

          &lt;span class="c"&gt;# the friendly interactive shell!&lt;/span&gt;
          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;fish&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="nv"&gt;shellHook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;valkey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/valkey-server &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;valkeyConf&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          exec fish&lt;/span&gt;&lt;span class="err"&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="p"&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;p&gt;The &lt;code&gt;default&lt;/code&gt; shell in the above flake adds Valkey, NodeJS &lt;code&gt;22&lt;/code&gt;, the PNPM package manager, and the &lt;code&gt;fish&lt;/code&gt; shell to the environment. It also starts Valkey in the background through a shell hook, passing it a custom config (declared via Nix!) and runs &lt;code&gt;fish&lt;/code&gt; so we're dropped in the &lt;a href="https://fishshell.com/" rel="noopener noreferrer"&gt;&lt;code&gt;fish&lt;/code&gt; shell&lt;/a&gt; instead of our login shell.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;exec&lt;/code&gt; command replaces the currently running process with the command specified. If &lt;code&gt;fish&lt;/code&gt; was run without &lt;code&gt;exec&lt;/code&gt;, it would drop you into your login shell with the shell environment (or run any additional commands in the &lt;code&gt;shellHook&lt;/code&gt; if there were any) after you exit fish instead of exiting the devshell as expected. With &lt;code&gt;exec&lt;/code&gt;, when you exit &lt;code&gt;fish&lt;/code&gt;, it'll exit the environment too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's enter the shell:&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="nv"&gt;$ &lt;/span&gt;nix develop
155483:C 15 Jan 2025 15:56:27.317 &lt;span class="k"&gt;*&lt;/span&gt; oO0OoO0OoO0Oo Valkey is starting oO0OoO0OoO0Oo
155483:C 15 Jan 2025 15:56:27.317 &lt;span class="k"&gt;*&lt;/span&gt; Valkey &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.0.1, &lt;span class="nv"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;64, &lt;span class="nv"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;00000000, &lt;span class="nv"&gt;modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0, &lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;155483, just started
155483:C 15 Jan 2025 15:56:27.317 &lt;span class="k"&gt;*&lt;/span&gt; Configuration loaded
155483:M 15 Jan 2025 15:56:27.318 &lt;span class="k"&gt;*&lt;/span&gt; monotonic clock: POSIX clock_gettime
                .+^+.
            .+#########+.
        .+########+########+.           Valkey 8.0.1 &lt;span class="o"&gt;(&lt;/span&gt;00000000/0&lt;span class="o"&gt;)&lt;/span&gt; 64 bit
    .+########+&lt;span class="s1"&gt;'     '&lt;/span&gt;+########+.
 .########+&lt;span class="s1"&gt;'     .+.     '&lt;/span&gt;+########.    Running &lt;span class="k"&gt;in &lt;/span&gt;standalone mode
 |####+&lt;span class="s1"&gt;'     .+#######+.     '&lt;/span&gt;+####|    Port: 12345
 |###|   .+###############+.   |###|    PID: 155483
 |###|   |#####&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="c"&gt;#####|   |###|&lt;/span&gt;
 |###|   |####&lt;span class="s1"&gt;'  .-.  '&lt;/span&gt;&lt;span class="c"&gt;####|   |###|&lt;/span&gt;
 |###|   |###&lt;span class="o"&gt;(&lt;/span&gt;  &lt;span class="o"&gt;(&lt;/span&gt;@@@&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="c"&gt;###|   |###|          https://valkey.io&lt;/span&gt;
 |###|   |####.  &lt;span class="s1"&gt;'-'&lt;/span&gt;  .####|   |###|
 |###|   |#####&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;   .&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="c"&gt;#####|   |###|&lt;/span&gt;
 |###|   &lt;span class="s1"&gt;'+#####|   |#####+'&lt;/span&gt;   |###|
 |####+.     +##|   |#+&lt;span class="s1"&gt;'     .+####|
 '&lt;/span&gt;&lt;span class="c"&gt;#######+   |##|        .+########'&lt;/span&gt;
    &lt;span class="s1"&gt;'+###|   |##|    .+########+'&lt;/span&gt;
        &lt;span class="s1"&gt;'|   |####+########+'&lt;/span&gt;
             +#########+&lt;span class="s1"&gt;'
                '&lt;/span&gt;+v+&lt;span class="s1"&gt;'

155483:M 15 Jan 2025 15:56:27.320 * Server initialized
155483:M 15 Jan 2025 15:56:27.320 * Loading RDB produced by Valkey version 8.0.1
155483:M 15 Jan 2025 15:56:27.320 * RDB age 2934 seconds
155483:M 15 Jan 2025 15:56:27.320 * RDB memory usage when created 0.91 Mb
155483:M 15 Jan 2025 15:56:27.320 * Done loading RDB, keys loaded: 0, keys expired: 0.
155483:M 15 Jan 2025 15:56:27.320 * DB loaded from disk: 0.001 seconds
155483:M 15 Jan 2025 15:56:27.320 * Ready to accept connections tcp

&amp;gt; ps 
   PID TTY          TIME CMD
 153101 pts/3    00:00:00 fish  # this is login shell
 153205 pts/3    00:00:04 fish  # this is the fish process started by `nix develop`
 155483 pts/3    00:00:00 valkey-server  # valkey is running in the background
 155654 pts/3    00:00:00 ps
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you see that valkey has started in the background! To exit valkey, you need to kill it yourself, like so:&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;kill &lt;/span&gt;155483
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If valkey isn't killed, it'll continue running in the background, even &lt;em&gt;after&lt;/em&gt; you exit the devshell!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to kill valkey when you exit the shell, you can change your &lt;code&gt;shellHook&lt;/code&gt; to kill the valkey process on exit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;shellHook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;valkey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/valkey-server &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;valkeyConf&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  fish&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  echo Killing valkey server&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  # `ps` lists all processes, `grep` searches for a line with `valkey-server`&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  # in it. `awk` grabs the first column (the PID) from the output, and &lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  # `xargs` sends stdin as arguments to `kill`.&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  ps | grep valkey-server | awk "{printf \$1}" | xargs kill&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  exit&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Improvement needed:&lt;/strong&gt; If you have another way to achieve this, then please let me know in the comments!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can't use &lt;code&gt;exec&lt;/code&gt; anymore, since we need to execute commands after &lt;code&gt;fish&lt;/code&gt; exits. &lt;/p&gt;

&lt;p&gt;Now running &lt;code&gt;nix develop&lt;/code&gt; starts valkey, drops you into a shell, and when you exit the shell, valkey will automatically be killed!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pinning package versions:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This usually isn't required, since flakes are already pinned to exact commits via. the &lt;code&gt;flake.lock&lt;/code&gt; file, which &lt;em&gt;should&lt;/em&gt; be checked into version control. So even if you enter a development shell ten years from now, you'll have the same version of all packages as the &lt;code&gt;flake.lock&lt;/code&gt; (provided that GitHub still exists :P). &lt;/p&gt;

&lt;p&gt;To update package versions, you can run &lt;code&gt;nix flake update&lt;/code&gt;, which will fetch the latest commit of the branch of all your inputs.&lt;/p&gt;

&lt;p&gt;But if you still want a specific version of a package, down to the patch version, you can use a &lt;code&gt;nixpkgs&lt;/code&gt; input that has exactly that package. Do note that you &lt;em&gt;may have to build that package yourself&lt;/em&gt; if the Nix build cache doesn't have it, as is common with quite old versions of packages.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before doing that, do &lt;a href="https://search.nixos.org/packages" rel="noopener noreferrer"&gt;search nixpkgs&lt;/a&gt; for alternate versions of packages that may exist, for example, nixpkgs has node 18, 20, and 22 in it. Many popular packages are split into different nixpkgs for major versions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the disclaimers out of the way, let's learn how you can pin a specific version of a package. For this example, we'll add Node 16 (an end-of-life version that you totally &lt;strong&gt;shouldn't&lt;/strong&gt; be using) to the environment.&lt;/p&gt;

&lt;p&gt;First, we need to find a commit of the &lt;code&gt;nixpkgs&lt;/code&gt; repo that has Node 16. The latest commit should be enough. There are tools like &lt;a href="//https//nixhub.io"&gt;NixHub&lt;/a&gt; that help with exactly that. If you check the page for &lt;a href="https://www.nixhub.io/packages/nodejs" rel="noopener noreferrer"&gt;&lt;code&gt;nodejs&lt;/code&gt; on NixHub&lt;/a&gt;, you can see all the versions of node that were ever published on &lt;code&gt;nixpkgs&lt;/code&gt;. Copy the &lt;code&gt;nixpkgs&lt;/code&gt; commit for &lt;code&gt;nodejs&lt;/code&gt; &lt;code&gt;16.20.2&lt;/code&gt;, i.e. the part in "Nixpkgs Reference", except the flake output name, but do keep it in mind (&lt;code&gt;#nodejs_16&lt;/code&gt;), which happens to be &lt;code&gt;a71323f68d4377d12c04a5410e214495ec598d4c&lt;/code&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%2F5dxuq4fk6geksz90mmnq.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%2F5dxuq4fk6geksz90mmnq.png" alt="A screenshot of NixHub for  raw `nodejs` endraw  version  raw `16.20.2` endraw , and the nixpkgs commit is highlighted with a black outline" width="800" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, add that to our environment like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Nix devshells!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:nixos/nixpkgs/nixos-24.11"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c"&gt;# NEW&lt;/span&gt;
    &lt;span class="nv"&gt;nodejs_16_nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:nixos/nixpkgs/a71323f68d4377d12c04a5410e214495ec598d4c"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nodejs_16_nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt;
    &lt;span class="nv"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x86_64-linux"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;inherit&lt;/span&gt; &lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c"&gt;# NEW&lt;/span&gt;
    &lt;span class="nv"&gt;nodejs_16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nv"&gt;nodejs_16_nixpkgs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;inherit&lt;/span&gt; &lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nodejs_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c"&gt;# ...&lt;/span&gt;
  &lt;span class="kn"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;devShells&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c"&gt;# ...&lt;/span&gt;
      &lt;span class="nv"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkShell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;valkey&lt;/span&gt;

          &lt;span class="c"&gt;# NEW: replaced node 22 and pnpm with node 16&lt;/span&gt;
          &lt;span class="nv"&gt;nodejs_16&lt;/span&gt;

          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;fish&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="c"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;nix develop&lt;/code&gt; will actually give us an error claiming that this version of Node is end-of-life, which it is.&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="nv"&gt;$ &lt;/span&gt;nix develop
error:
       … &lt;span class="k"&gt;while &lt;/span&gt;calling the &lt;span class="s1"&gt;'derivationStrict'&lt;/span&gt; &lt;span class="nb"&gt;builtin
         &lt;/span&gt;at &amp;lt;nix/derivation-internal.nix&amp;gt;:34:12:
           33|
           34|   strict &lt;span class="o"&gt;=&lt;/span&gt; derivationStrict drvAttrs&lt;span class="p"&gt;;&lt;/span&gt;
             |            ^
           35|

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating derivation &lt;span class="s1"&gt;'nix-shell'&lt;/span&gt;
         whose name attribute is located at /nix/store/2csx2kkb2hxyxhhmg2xs9jfyypikwwk6-source/pkgs/stdenv/generic/make-derivation.nix:336:7

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating attribute &lt;span class="s1"&gt;'nativeBuildInputs'&lt;/span&gt; of derivation &lt;span class="s1"&gt;'nix-shell'&lt;/span&gt;
         at /nix/store/2csx2kkb2hxyxhhmg2xs9jfyypikwwk6-source/pkgs/stdenv/generic/make-derivation.nix:380:7:
          379|       depsBuildBuild              &lt;span class="o"&gt;=&lt;/span&gt; elemAt &lt;span class="o"&gt;(&lt;/span&gt;elemAt dependencies 0&lt;span class="o"&gt;)&lt;/span&gt; 0&lt;span class="p"&gt;;&lt;/span&gt;
          380|       nativeBuildInputs           &lt;span class="o"&gt;=&lt;/span&gt; elemAt &lt;span class="o"&gt;(&lt;/span&gt;elemAt dependencies 0&lt;span class="o"&gt;)&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt;
             |       ^
          381|       depsBuildTarget             &lt;span class="o"&gt;=&lt;/span&gt; elemAt &lt;span class="o"&gt;(&lt;/span&gt;elemAt dependencies 0&lt;span class="o"&gt;)&lt;/span&gt; 2&lt;span class="p"&gt;;&lt;/span&gt;

       &lt;span class="o"&gt;(&lt;/span&gt;stack trace truncated&lt;span class="p"&gt;;&lt;/span&gt; use &lt;span class="s1"&gt;'--show-trace'&lt;/span&gt; to show the full, detailed trace&lt;span class="o"&gt;)&lt;/span&gt;

       error: Package ‘nodejs-16.20.2’ &lt;span class="k"&gt;in&lt;/span&gt; /nix/store/bxygxxbgcc7s82wn8a8wdp4gd34hiw4w-source/pkgs/development/web/nodejs/v16.nix:28 is marked as insecure, refusing to evaluate.


       Known issues:
        - This NodeJS release has reached its end of life. See https://nodejs.org/en/about/releases/.

       You can &lt;span class="nb"&gt;install &lt;/span&gt;it anyway by allowing this package, using the
       following methods:

       a&lt;span class="o"&gt;)&lt;/span&gt; To temporarily allow all insecure packages, you can use an environment
          variable &lt;span class="k"&gt;for &lt;/span&gt;a single invocation of the nix tools:

            &lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NIXPKGS_ALLOW_INSECURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

          Note: When using &lt;span class="sb"&gt;`&lt;/span&gt;nix shell&lt;span class="sb"&gt;`&lt;/span&gt;, &lt;span class="sb"&gt;`&lt;/span&gt;nix build&lt;span class="sb"&gt;`&lt;/span&gt;, &lt;span class="sb"&gt;`&lt;/span&gt;nix develop&lt;span class="sb"&gt;`&lt;/span&gt;, etc with a flake,
                &lt;span class="k"&gt;then &lt;/span&gt;pass &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--impure&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;order to allow use of environment variables.

       b&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;nixos-rebuild&lt;span class="sb"&gt;`&lt;/span&gt; you can add ‘nodejs-16.20.2’ to
          &lt;span class="sb"&gt;`&lt;/span&gt;nixpkgs.config.permittedInsecurePackages&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;the configuration.nix,
          like so:

            &lt;span class="o"&gt;{&lt;/span&gt;
              nixpkgs.config.permittedInsecurePackages &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"nodejs-16.20.2"&lt;/span&gt;
              &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;

       c&lt;span class="o"&gt;)&lt;/span&gt; For &lt;span class="sb"&gt;`&lt;/span&gt;nix-env&lt;span class="sb"&gt;`&lt;/span&gt;, &lt;span class="sb"&gt;`&lt;/span&gt;nix-build&lt;span class="sb"&gt;`&lt;/span&gt;, &lt;span class="sb"&gt;`&lt;/span&gt;nix-shell&lt;span class="sb"&gt;`&lt;/span&gt; or any other Nix &lt;span class="nb"&gt;command &lt;/span&gt;you can add
          ‘nodejs-16.20.2’ to &lt;span class="sb"&gt;`&lt;/span&gt;permittedInsecurePackages&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
          ~/.config/nixpkgs/config.nix, like so:

            &lt;span class="o"&gt;{&lt;/span&gt;
              permittedInsecurePackages &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"nodejs-16.20.2"&lt;/span&gt;
              &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fortunately, the error also provides an easy fix:&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="nv"&gt;$ NIXPKGS_ALLOW_INSECURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 nix develop &lt;span class="nt"&gt;--impure&lt;/span&gt;
&lt;span class="c"&gt;# valkey output truncated ...&lt;/span&gt;
&lt;span class="c"&gt;# you can append &amp;gt; /dev/null to the valkey command in shellHook&lt;/span&gt;
&lt;span class="c"&gt;# to supress this output&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;
v16.20.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success! You're now using an old, unsupported version of node!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Be warned! If you're reading this article in the future, and decide to use this version of NodeJS, then don't be surprised if you have to build NodeJS from scratch! (It's not a fast build, by the way).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Automatic shell environments with &lt;code&gt;direnv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A devshell discussion with Nix is not complete without introducing &lt;a href="https://direnv.net" rel="noopener noreferrer"&gt;&lt;code&gt;direnv&lt;/code&gt;&lt;/a&gt;. &lt;code&gt;direnv&lt;/code&gt; is a tool that &lt;em&gt;very simply&lt;/em&gt; runs commands based on the current directory. It is tedious to run &lt;code&gt;nix develop&lt;/code&gt; to get into a development shell every time. It's also tedious to remember to exit the dev shell when you're done. &lt;code&gt;direnv&lt;/code&gt; automatically does this for you, so it's a valuable addition! It also allows you to use these shells within non-terminal editors like VSCode and JetBrains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just a small fix:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a small thing we need to change in &lt;code&gt;flake.nix&lt;/code&gt;. It's quite a bad idea to start Valkey every time the devshell is opened, since more than one instance of a dev shell can exist at the same time. Instead, wrap the valkey command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# ...&lt;/span&gt;
  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nodejs_16_nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt;
    &lt;span class="c"&gt;# ...&lt;/span&gt;

    &lt;span class="c"&gt;# NEW&lt;/span&gt;
    &lt;span class="nv"&gt;valkeyScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;writeShellScriptBin&lt;/span&gt;
      &lt;span class="s2"&gt;"start-valkey"&lt;/span&gt;
      &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;        &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;valkey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/valkey-server &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;valkeyConf&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;      ''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;devShells&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c"&gt;# ...&lt;/span&gt;
      &lt;span class="nv"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkShell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;valkey&lt;/span&gt;

          &lt;span class="nv"&gt;nodejs_16&lt;/span&gt;

          &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;fish&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="nv"&gt;shellHook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          # NEW: Replace valkey startup and shutdown code with this&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          export PATH="&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;valkeyScript&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin:$PATH"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          # NEW: Add exec back&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          exec fish&lt;/span&gt;&lt;span class="err"&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="p"&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;p&gt;This change adds a shell script called &lt;code&gt;start-valkey&lt;/code&gt; to the environment, which runs valkey with the specified configuration. Now, to start valkey, you just run &lt;code&gt;start-valkey&lt;/code&gt;, and press &lt;code&gt;Ctrl-C&lt;/code&gt; to stop it. This allows us to have multiple shells in that environment now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install &lt;code&gt;direnv&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;direnv&lt;/code&gt; is packaged in most linux distros, but since this is a Nix guide after all, let's install it via. nix by running this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix profile &lt;span class="nb"&gt;install &lt;/span&gt;nixpkgs#direnv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also need to &lt;a href="https://direnv.net/docs/hook.html" rel="noopener noreferrer"&gt;hook &lt;code&gt;direnv&lt;/code&gt; into your shell&lt;/a&gt;, since &lt;code&gt;direnv&lt;/code&gt; needs to know when you've changed directories. Follow the instructions on &lt;a href="https://direnv.net/docs/hook.html" rel="noopener noreferrer"&gt;this page&lt;/a&gt; for your shell. For &lt;code&gt;bash&lt;/code&gt;, you need to add &lt;code&gt;eval "$(direnv hook bash)"&lt;/code&gt; to the end of your &lt;code&gt;~/.bashrc&lt;/code&gt;, and restart your terminal after that.&lt;/p&gt;

&lt;p&gt;To tell &lt;code&gt;direnv&lt;/code&gt; we want it to start the development shell when we &lt;code&gt;cd&lt;/code&gt; into this folder, add an &lt;code&gt;.envrc&lt;/code&gt; in the folder, with the following contents:&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="c"&gt;# Need to set `stty sane` for fish&lt;/span&gt;
&lt;span class="c"&gt;# https://github.com/direnv/direnv/issues/967#issuecomment-1987134113&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$SHELL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="s2"&gt;"fish"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;stty &lt;/span&gt;sane
&lt;span class="k"&gt;fi

&lt;/span&gt;use flake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will make &lt;code&gt;direnv&lt;/code&gt; automatically enter your development environment. Just run &lt;code&gt;direnv allow&lt;/code&gt;, to allow executing this &lt;code&gt;.envrc&lt;/code&gt;, and you're automatically dropped into the development environment, with glorious Node 16 available!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're using &lt;code&gt;fish&lt;/code&gt; as your login shell, remove &lt;code&gt;exec fish&lt;/code&gt; from the &lt;code&gt;shellHook&lt;/code&gt; in the flake, otherwise &lt;code&gt;direnv&lt;/code&gt; will go on an infinite loop, since the &lt;code&gt;fish&lt;/code&gt; spawned by the devshell spawns &lt;code&gt;direnv&lt;/code&gt; which sees that it should enter the devshell, which spawns a &lt;code&gt;fish&lt;/code&gt;, which spawns a &lt;code&gt;direnv&lt;/code&gt;, ... and so on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Other alternatives to development shells built with Nix
&lt;/h2&gt;

&lt;p&gt;If writing a devshell on your own seems more complicated than necessary, you can use tools like &lt;a href="https://devenv.sh" rel="noopener noreferrer"&gt;Devenv&lt;/a&gt; or &lt;a href="https://jetify.com/devbox" rel="noopener noreferrer"&gt;Devbox&lt;/a&gt; (by the same team that built &lt;a href="https://nixhub.io" rel="noopener noreferrer"&gt;NixHub&lt;/a&gt;), which are both built on Nix. Devenv provides nice wrappers to automatically add languages, services (like postgres or redis), etc. on top of your flake, without having to do the shenanigans we had to do with Valkey. Devbox on the other hand, lets you skip writing Nix entirely, since they have their own CLI and lock file that pull packages from nixpkgs.&lt;/p&gt;

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

&lt;p&gt;You've now learned one of the most powerful features of nix! In the next few articles, we'll cover packaging for Nix!&lt;/p&gt;

&lt;p&gt;If you really liked this article and would like to support me, here are some ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sponsors/arnu515" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://m.do.co/c/371591aa3027" rel="noopener noreferrer"&gt;Use my DigitalOcean referral link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.prisma.io/?via=arnu515" rel="noopener noreferrer"&gt;Use my Prisma referral link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you so much!&lt;/p&gt;

</description>
      <category>nix</category>
      <category>tutorial</category>
      <category>linux</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Getting started with Nix and Nix Flakes</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Fri, 03 Jan 2025 12:26:17 +0000</pubDate>
      <link>https://dev.to/arnu515/getting-started-with-nix-and-nix-flakes-mml</link>
      <guid>https://dev.to/arnu515/getting-started-with-nix-and-nix-flakes-mml</guid>
      <description>&lt;p&gt;Let's get started with &lt;a href="https://nixos.org" rel="noopener noreferrer"&gt;Nix&lt;/a&gt;! This article guides you into setting up the Nix package manager, along with &lt;em&gt;flakes&lt;/em&gt;, and demonstrates some cool things it can do.&lt;/p&gt;

&lt;p&gt;For a quick intro to what Nix is, check out &lt;a href="https://dev.to/arnu515/my-new-nix-series-2cc3"&gt;this article I posted&lt;/a&gt;, which introduces Nix, and this series of articles to you. This article demonstrates an overview of the Nix package manager, more Nix concepts will be covered in future articles of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Nix
&lt;/h2&gt;

&lt;p&gt;The Nix package manager can be installed on both Linux and Mac, and is also available as a &lt;a href="https://hub.docker.com/r/nixos/nix" rel="noopener noreferrer"&gt;Docker image&lt;/a&gt; for you to try out without installing it on bare metal. Windows users will have to use WSL2 to install Nix on their systems (if they're not using Docker).&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nixos.org" rel="noopener noreferrer"&gt;Nix website&lt;/a&gt;'s &lt;a href="https://nixos.org/download" rel="noopener noreferrer"&gt;download page&lt;/a&gt; guides you into using their installer to install Nix on your system. However, this article will use [Determinate Systems' Nix installer] instead, since it lets you easily &lt;a href="https://zero-to-nix.com/start/uninstall" rel="noopener noreferrer"&gt;undo all changes&lt;/a&gt; their Nix installer makes (i.e. uninstall) with one command, and it also enables &lt;em&gt;Nix flakes&lt;/em&gt; by default, which you'd have to enable on your own if you were using the official Nix installer instead.&lt;/p&gt;

&lt;p&gt;Their guide, &lt;a href="https://zero-to-nix.com" rel="noopener noreferrer"&gt;Zero-to-Nix&lt;/a&gt; has &lt;a href="https://zero-to-nix.com/start/install/" rel="noopener noreferrer"&gt;detailed installation instructions&lt;/a&gt; using the Determinate installer, but in essence, it just boils down to running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-sSf&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; https://install.determinate.systems/nix | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in a terminal and following the prompts the installer asks. You can check &lt;a href="https://zero-to-nix.com/start/install/" rel="noopener noreferrer"&gt;Zero-to-Nix's page&lt;/a&gt; for more instructions, or the &lt;a href="https://github.com/DeterminateSystems/nix-installer" rel="noopener noreferrer"&gt;Determinate installer's README&lt;/a&gt; for detailed options. This method will work both on Linux and Mac.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Obligatory disclaimer:&lt;/strong&gt; Nix flakes, and nix experimental commands (like &lt;code&gt;nix shell&lt;/code&gt; and &lt;code&gt;nix profile&lt;/code&gt;, which will be covered later) are experimental and may have breaking changes anytime in the future, hence, they are gated by a config option when using the default installer. But these two features have stayed experimental for many years with little-to-no breaking changes, and are thus considered &lt;em&gt;de facto&lt;/em&gt; stable by much of the Nix community, and &lt;em&gt;not&lt;/em&gt; using them is just giving yourself a handicap for no reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the official installer?
&lt;/h3&gt;

&lt;p&gt;If you've opted to use the &lt;a href="https://nixos.org/download" rel="noopener noreferrer"&gt;official Nix installer&lt;/a&gt; instead, you'll have to manually enable flakes for your user by editing &lt;code&gt;~/.config/nix/nix.conf&lt;/code&gt;, or for all users by editing &lt;code&gt;/etc/nix/nix.conf&lt;/code&gt; and adding the following line to the end of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;-&lt;span class="n"&gt;features&lt;/span&gt; = &lt;span class="n"&gt;nix&lt;/span&gt;-&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="n"&gt;flakes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You'll have to create the file if it doesn't already exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  No Systemd?
&lt;/h3&gt;

&lt;p&gt;If you're using a systemd-less distro, like &lt;a href="https://artixlinux.org" rel="noopener noreferrer"&gt;Artix&lt;/a&gt; or &lt;a href="https://voidlinux.org" rel="noopener noreferrer"&gt;Void&lt;/a&gt;, you can install Nix from their repositories, since it will come preconfigured with whatever init system you'd be using on those distros. The Determinate installer only works on distros with systemd (for a multi-user installation, which is what you want).&lt;/p&gt;

&lt;p&gt;If you wish, you can install Nix from your distro's repositories too, instead of using the Determinate installer. I've done this on Alpine, Artix, and Arch. Just make sure the Nix package is not extremely out of date, like in Void's case. You can check if the &lt;code&gt;nix&lt;/code&gt; package is out of date by comparing the version in your repos to &lt;a href="https://search.nixos.org/packages?channel=unstable&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=relevance&amp;amp;type=packages&amp;amp;query=nix" rel="noopener noreferrer"&gt;the version on NixOS's repos&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Void linux's Nix package is very out-of-date (by 2-ish years!) as of the time of writing. You'll have to use the &lt;a href="https://nixos.org/manual/nix/stable/installation/single-user" rel="noopener noreferrer"&gt;Single-user installation&lt;/a&gt; mode of the &lt;a href="https://nixos.org/download/#nix-install-linux" rel="noopener noreferrer"&gt;official Nix installer&lt;/a&gt; to install Nix on void.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Using Nix packages
&lt;/h2&gt;

&lt;p&gt;Now that you have the Nix package manager installed, you can use it to install &lt;em&gt;any&lt;/em&gt; package from the &lt;a href="https://search.nixos.org/packages" rel="noopener noreferrer"&gt;vast library of 120k+ Nix packages&lt;/a&gt; on your system, while ensuring that none of your other packages, even those installed by Nix itself, will break due to dependency conflicts.&lt;/p&gt;

&lt;p&gt;Let's install the &lt;a href="https://github.com/busyloop/lolcat" rel="noopener noreferrer"&gt;lolcat&lt;/a&gt; package. Well, Nix actually allows us to try out the package &lt;em&gt;without installing it&lt;/em&gt; first! It downloads the package (or compiles it if the binary isn't availabe in the &lt;a href="https://cache.nixos.org" rel="noopener noreferrer"&gt;build cache&lt;/a&gt;) and drops you into a shell session containing the requested package in your &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The below demonstrations will only have their intended effect if you &lt;em&gt;don't&lt;/em&gt; already have &lt;code&gt;lolcat&lt;/code&gt; installed!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's try it out! Run &lt;code&gt;nix shell nixpkgs#lolcat&lt;/code&gt; and Nix will download the latest commit of &lt;a href="https://github.com/nixos/nixpkgs" rel="noopener noreferrer"&gt;nixpkgs&lt;/a&gt;, a collection of lots of Nix packages, find the &lt;code&gt;lolcat&lt;/code&gt; package, get its binary from NixOS's &lt;a href="https://cache.nixos.org" rel="noopener noreferrer"&gt;binary cache&lt;/a&gt;, download it and put it in your &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nix will create you a new shell session where &lt;code&gt;lolcat&lt;/code&gt; will be available.&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%2Fcfvv0voppgar8qlkoktd.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%2Fcfvv0voppgar8qlkoktd.png" alt="A screenshot of a shell session with two commands:  raw `nix shell nixpkgs#lolcat` endraw , which has no output, and  raw `echo " width="390" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✨🪄 Magic! Open a new terminal window, try running &lt;code&gt;lolcat&lt;/code&gt;, and see that the command doesn't exist! Let's see what Nix added to our &lt;code&gt;$PATH&lt;/code&gt; to make &lt;code&gt;lolcat&lt;/code&gt; available:&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="nv"&gt;$ &lt;/span&gt;nix shell nixpkgs#lolcat

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$PATH&lt;/span&gt;
/nix/store/3mbkj2nlzf87aapwp1ckqrid21p9lb3j-lolcat-100.0.1/bin:... &lt;span class="c"&gt;# (truncated)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nix downloaded &lt;code&gt;lolcat&lt;/code&gt; and placed it in a folder in the Nix store (&lt;code&gt;/nix/store&lt;/code&gt; by default). The Nix store contains all packages, even multiple versions of the same package that were ever fetched by Nix. My system, about a month old, has close to 34k items in the store! Some of these are packages, some are built derivations (more on those later!). This can be cleaned using the &lt;code&gt;nix store gc&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;We'll learn more about the nix store in further articles, but for a quick rundown, the Nix store is a read-only filesystem which stores things like downloaded and built packages, any packages you create yourself, and anything else included in a nix package like downloaded or locally available source code. All packages are treated as a &lt;em&gt;pure function&lt;/em&gt;, and their built output is stored in the store.&lt;/p&gt;

&lt;p&gt;Notice the name of the directory where &lt;code&gt;lolcat&lt;/code&gt; was downloaded. It contains the hash of the derivation, ensuring integrity of the package, then the name of the package itself, and finally, the version, which in this case is &lt;code&gt;100.0.1&lt;/code&gt;. When you're trying out these commands for yourself, you may have a different hash and/or version of the package.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lolcat&lt;/code&gt; is specified to the &lt;code&gt;nix shell&lt;/code&gt; command as &lt;code&gt;nixpkgs#lolcat&lt;/code&gt;. This is termed as an &lt;strong&gt;installable&lt;/strong&gt;, and in this case, is a &lt;strong&gt;flake reference&lt;/strong&gt; (more about that later). &lt;code&gt;nixpkgs#lolcat&lt;/code&gt; is actually a URL with path &lt;code&gt;nixpkgs&lt;/code&gt;, and fragment, i.e. the part after the &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;lolcat&lt;/code&gt;. &lt;code&gt;nixpkgs&lt;/code&gt; is an alias which resolves to the &lt;a href="https://github.com/nixos/nixpkgs/tree/nixpkgs-unstable" rel="noopener noreferrer"&gt;&lt;code&gt;nixpkgs-unstable&lt;/code&gt;&lt;/a&gt; branch of the &lt;a href="https://github.com/nixos/nixpkgs" rel="noopener noreferrer"&gt;Nixpkgs GitHub repository&lt;/a&gt;, a vast collection of Nix packages. There are other stable branches of nixpkgs, like &lt;code&gt;nixpkgs-24.11&lt;/code&gt;, the latest one as of writing. There are quite a few other aliases too, Nix downloads the list from &lt;a href="https://channels.nixos.org/flake-registry.json" rel="noopener noreferrer"&gt;this JSON file&lt;/a&gt;. If you omit the URL host, it defaults to the current directory (&lt;code&gt;.&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;From the JSON file, the alias for &lt;code&gt;nixpkgs&lt;/code&gt; appears to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nixpkgs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"indirect"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NixOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nixpkgs-unstable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nixpkgs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The object at &lt;code&gt;"to"&lt;/code&gt; is one type of flake reference. It can also be written in a URL form like so: &lt;code&gt;github:NixOS/nixpkgs/nixpkgs-unstable&lt;/code&gt;. We'll take a look at various types of flake references later.&lt;/p&gt;

&lt;p&gt;The fragment of the URL, &lt;code&gt;lolcat&lt;/code&gt;, refers to one of the packages exported by the &lt;code&gt;nixpkgs&lt;/code&gt; flake. Omitting it defaults to the &lt;code&gt;default&lt;/code&gt; package, i.e., literally a package called &lt;code&gt;default&lt;/code&gt;. We'll talk more about flakes later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Actually installing &lt;code&gt;lolcat&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Having &lt;code&gt;lolcat&lt;/code&gt; in a local shell isn't really useful. Let's install &lt;code&gt;lolcat&lt;/code&gt; using Nix so that it can be accessed from any shell (that has the Nix profile in &lt;code&gt;$PATH&lt;/code&gt;!).&lt;/p&gt;

&lt;p&gt;A nix profile is a set of packages that are installed independently from each other. Nix profiles are versioned, so you can roll back to a previous state of your profile at any time!&lt;/p&gt;

&lt;p&gt;To install &lt;code&gt;lolcat&lt;/code&gt; into your profile, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix profile &lt;span class="nb"&gt;install &lt;/span&gt;nixpkgs#lolcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;lolcat&lt;/code&gt; will be available in any shell! To remove it, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix profile remove lolcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lolcat&lt;/code&gt; will no longer be available in path, but it &lt;em&gt;will&lt;/em&gt; still remain in the Nix store. If you wish to use &lt;code&gt;lolcat&lt;/code&gt; again, maybe in a temporary shell, it will not have to be downloaded/built again, since it'll already be available in the Nix store.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Notice that this time we pass the package name only, and not a flake reference. To view a list of installed packages in your current profile, run &lt;code&gt;nix profile list&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, let's demonstrate profile versioning. Run &lt;code&gt;nix profile history&lt;/code&gt; to see the versions of your profile:&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="nv"&gt;$ &lt;/span&gt;nix profile &lt;span class="nb"&gt;history
&lt;/span&gt;Version 1 &lt;span class="o"&gt;(&lt;/span&gt;2025-01-03&lt;span class="o"&gt;)&lt;/span&gt;:
  flake:nixpkgs#legacyPackages.x86_64-linux.lolcat: ∅ -&amp;gt; 100.0.1

Version 2 &lt;span class="o"&gt;(&lt;/span&gt;2025-01-03&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;- 1:
  flake:nixpkgs#legacyPackages.x86_64-linux.lolcat: 100.0.1 -&amp;gt; ∅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;2 will be green, since it is the current version of the profile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't worry about &lt;code&gt;flake:nixpkgs#legacyPackages.x86_64-linux.lolcat&lt;/code&gt; for now, that'll be covered later. Just notice that the first history version installed &lt;code&gt;lolcat&lt;/code&gt; &lt;code&gt;100.0.1&lt;/code&gt;, and the second history version removed it. Let's roll back to the first version with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix profile rollback &lt;span class="nt"&gt;--to&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;lolcat&lt;/code&gt; is back! If we make a change while we're in this version, Nix'll create a new version for us, without deleting version &lt;code&gt;2&lt;/code&gt;, so you can roll back to any point! For now, let's stick to the same slate, and go back to version &lt;code&gt;2&lt;/code&gt;. I'd leave it as an exercise for you to do the same.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you wish to learn more about these commands, you can append &lt;code&gt;--help&lt;/code&gt; to see a nicely formatted colored manual.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Nix language basics
&lt;/h2&gt;

&lt;p&gt;This article will not teach you the Nix language, since there are much better places to learn that from, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://learnxinyminutes.com/nix/" rel="noopener noreferrer"&gt;https://learnxinyminutes.com/nix/&lt;/a&gt; (concise)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nix.dev/tutorials/nix-language" rel="noopener noreferrer"&gt;https://nix.dev/tutorials/nix-language&lt;/a&gt; (official)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nixcloud.io/tour/?id=introduction/nix" rel="noopener noreferrer"&gt;https://nixcloud.io/tour/?id=introduction/nix&lt;/a&gt; (interactive)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nix.dev/manual/nix/2.24/language/" rel="noopener noreferrer"&gt;https://nix.dev/manual/nix/2.24/language/&lt;/a&gt; (the full reference)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;highly recommended&lt;/strong&gt; to learn this language before proceeding to the next section. If you know the language, you'll not be troubled with the syntax when you make your own flakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  An introduction to flakes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nix.dev/concepts/flakes" rel="noopener noreferrer"&gt;Nix Flakes&lt;/a&gt; are an opinionated way to structure a nix expression made up of packages, OS configurations, development shells, modules, images, overlays, etc. If you've read the &lt;a href="https://dev.to/arnu515/my-new-nix-series-2cc3"&gt;series introduction&lt;/a&gt;, you'll know that every &lt;code&gt;.nix&lt;/code&gt; file is just a Nix expression.&lt;/p&gt;

&lt;p&gt;Before flakes were a thing, you'd have to create separate nix files for development shells (&lt;code&gt;shell.nix&lt;/code&gt;), packages (&lt;code&gt;default.nix&lt;/code&gt;), OS configurations (&lt;code&gt;configuration.nix&lt;/code&gt;), etc., which may get annoying, but is just a small hindrance. The real advantage of flakes is flake inputs, which let you easily fetch nix packages from anywhere using many methods ("fetchers"), and the &lt;code&gt;nix&lt;/code&gt; experimental commands, which are built to work with flakes.&lt;/p&gt;

&lt;p&gt;When you create a &lt;code&gt;flake.nix&lt;/code&gt; file in a directory, that directory becomes a flake, so it can be used as a path in the URL passed to &lt;code&gt;nix&lt;/code&gt; commands. Open an empty directory on your computer, and create a blank file called &lt;code&gt;flake.nix&lt;/code&gt; in it. That directory has now become a flake!&lt;/p&gt;

&lt;p&gt;We can run the simplest &lt;code&gt;nix&lt;/code&gt; command to verify that, &lt;code&gt;nix eval&lt;/code&gt; evaluates a Nix expression and prints it to stdout. Without any arguments, it &lt;em&gt;evaluates&lt;/em&gt; the &lt;strong&gt;default package&lt;/strong&gt; exported by the flake. Notice the emphasis on &lt;em&gt;evaluates&lt;/em&gt;, not &lt;em&gt;runs&lt;/em&gt; or &lt;em&gt;installs&lt;/em&gt;, i.e. the command just prints the package &lt;a href="https://nix.dev/tutorials/nix-language.html#derivations" rel="noopener noreferrer"&gt;derivation&lt;/a&gt; out to the screen. Some things are &lt;em&gt;lazily evaluated&lt;/em&gt;, meaning they aren't evaluated until they're required. A package in a flake is not evaluated when you're just using the flake's development shell. It is only evaluated when you use the package, i.e. when you install it, or use it in another flake.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;nix eval&lt;/code&gt; on an empty &lt;code&gt;flake.nix&lt;/code&gt; however, will give you a syntax error, well obviously, since an empty file is not a valid nix expression. A flake is an &lt;a href="https://nix.dev/tutorials/nix-language.html#attribute-set" rel="noopener noreferrer"&gt;attrset&lt;/a&gt; which has a few fields as described &lt;a href="https://nix.dev/manual/nix/2.24/command-ref/new-cli/nix3-flake.html#flake-format" rel="noopener noreferrer"&gt;here&lt;/a&gt;, notably &lt;code&gt;inputs&lt;/code&gt;, a set of &lt;em&gt;other flakes&lt;/em&gt; your flake uses, and &lt;code&gt;outputs&lt;/code&gt;, a function returning a set of things your flake exposes (which can be used by other flakes!). The declared flakes in &lt;code&gt;inputs&lt;/code&gt; will be fetched by Nix, evaluated (lazily), and passed to the &lt;code&gt;outputs&lt;/code&gt; function, along with a special parameter &lt;code&gt;self&lt;/code&gt;, which is just a reference to the set returned by &lt;code&gt;outputs&lt;/code&gt;, so you can reference your own flake in itself. The power of lazy evaluation!&lt;/p&gt;

&lt;p&gt;Let's start with a simple &lt;code&gt;"Hello, world!"&lt;/code&gt; flake. Create &lt;code&gt;flake.nix&lt;/code&gt; in an empty directory on the machine you installed Nix in, and write the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"My first flake!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:nixos/nixpkgs/nixpkgs-24.11"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;x86_64-linux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;hello&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;legacyPackages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;x86_64-linux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;hello&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how a nix flake looks like. It looks quite a lot like JSON, but with semicolons instead of commas, but don't be fooled, Nix is a proper functional language with programming constructs and everything! The &lt;code&gt;inputs&lt;/code&gt; attribute set (attrset) declares a set of flakes your flake depends on. In this case, this flake depends on the &lt;code&gt;nixpkgs&lt;/code&gt; flake, a collection of 120k+ nix packages. The &lt;code&gt;nixpkgs&lt;/code&gt; flake's source code is hosted on GitHub at &lt;a href="https://github.com/nixos/nixpkgs" rel="noopener noreferrer"&gt;nixos/nixpkgs&lt;/a&gt;, which is what is specified in the flake input. The &lt;code&gt;nixpkgs-24.11&lt;/code&gt; part after the last &lt;code&gt;/&lt;/code&gt; is the branch of the repository. Nixpkgs releases a stable branch every six months, in May (&lt;code&gt;05&lt;/code&gt;), and November (&lt;code&gt;11&lt;/code&gt;, which is what we're using) every year. You can use &lt;code&gt;nixpkgs-unstable&lt;/code&gt; if you want the latest packages instead. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;outputs&lt;/code&gt; function shows one such construct. Nix attrsets can be condensed with dots, so &lt;code&gt;nixpkgs.url = "foo";&lt;/code&gt; is actually &lt;code&gt;nixpkgs = { url = "foo"; };&lt;/code&gt;, similarly with &lt;code&gt;packages.aarch64_linux.hello&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The function syntax of Nix is: &lt;code&gt;param: returned_expr&lt;/code&gt;. Nix functions can take in only one parameter, so multiple parameter functions are actually multiple functions with one parameter each, like so: &lt;code&gt;param1: param2: ...: returned_expr&lt;/code&gt;. Nix has support for attrset destructuring, so &lt;code&gt;{nixpkgs, ...}&lt;/code&gt; means that the parameter to &lt;code&gt;outputs&lt;/code&gt; is actually an attrset, and we extract the property &lt;code&gt;nixpkgs&lt;/code&gt;, which matches the input property from it. The &lt;code&gt;...&lt;/code&gt; means to ignore any extra properties passed. If it isn't specified, nix will error if properties other than &lt;code&gt;nixpkgs&lt;/code&gt; are passed! We know that &lt;code&gt;self&lt;/code&gt; is another property passed to output, hence &lt;code&gt;...&lt;/code&gt; is needed.&lt;/p&gt;

&lt;p&gt;Finally, in the outputs, we define a single package named &lt;code&gt;hello&lt;/code&gt; for 64-bit linux systems (which is what I have), which is just set to the &lt;code&gt;hello&lt;/code&gt; package declared by &lt;code&gt;nixpkgs&lt;/code&gt; for 64-bit linux systems. Note that the &lt;code&gt;legacy&lt;/code&gt; in &lt;code&gt;legacyPackages&lt;/code&gt; doesn't actually mean that these packages are legacy (see &lt;a href="https://github.com/NixOS/nixpkgs/blob/b2e41a5bd20d4114f27fe8d96e84db06b841d035/flake.nix#L47-L55" rel="noopener noreferrer"&gt;this&lt;/a&gt; for an explanation [TLDR; &lt;code&gt;legacyPackages&lt;/code&gt; makes the &lt;code&gt;nix flake show&lt;/code&gt; command not evaluate the package, apart from that, they're functionally identical]). You should change &lt;code&gt;x86_64&lt;/code&gt; and &lt;code&gt;linux&lt;/code&gt; to your own system's architecture and OS, if they differ from these.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For example, an M-series Mac would be &lt;code&gt;aarch64-darwin&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now running &lt;code&gt;nix eval&lt;/code&gt; tells us that there's no default package, which is true, since we created a package named &lt;code&gt;hello&lt;/code&gt;, not one named &lt;code&gt;default&lt;/code&gt;. We could rename &lt;code&gt;hello&lt;/code&gt; to &lt;code&gt;default&lt;/code&gt;, or we could also ask &lt;code&gt;nix eval&lt;/code&gt; to evaluate the &lt;code&gt;hello&lt;/code&gt; program, since it takes an &lt;code&gt;installable&lt;/code&gt; as an argument:&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="nv"&gt;$ &lt;/span&gt;nix &lt;span class="nb"&gt;eval&lt;/span&gt; .#hello
«derivation /nix/store/83p9zy4d8lh5fnipz7d1hl7g3rryw6mx-hello-2.12.1.drv»
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;.&lt;/code&gt; is technically optional, but if it is omitted, bash treats &lt;code&gt;#hello&lt;/code&gt; as a comment, so you'll have to quote it. I prefer putting a leading &lt;code&gt;.&lt;/code&gt; instead of quoting the whole string.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We get a derivation! Nix saw that our flake exports the &lt;code&gt;hello&lt;/code&gt; package or legacyPackage, saw that it is supposed to fetch the &lt;code&gt;hello&lt;/code&gt; package from the &lt;code&gt;nixpkgs&lt;/code&gt; flakes, fetches that flake if reqiured, and returns the derivation to us.&lt;/p&gt;

&lt;p&gt;We can also build and run this package:&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="nv"&gt;$ &lt;/span&gt;nix build .#hello
&lt;span class="c"&gt;# no output&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;./result/bin/hello
Hello, world!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;hello&lt;/code&gt; executable is actually GNU hello, that in true GNU fashion is an overly complicated program with a &lt;a href="https://www.gnu.org/software/hello/manual/hello.pdf" rel="noopener noreferrer"&gt;seventeen page manual&lt;/a&gt; that prints something to the screen, defaulting to &lt;code&gt;Hello, world!&lt;/code&gt;. Nix would build this program using its derivation from scratch, if it wasn't available in the &lt;a href="https://cache.nixos.org/" rel="noopener noreferrer"&gt;build cache&lt;/a&gt;, which most packages usually are, so it downloads the program from there instead. The derivation and built (or downloaded) output is stored in the Nix store, as we saw earlier. The &lt;code&gt;result&lt;/code&gt; symlink also links to a folder in the store. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;result&lt;/code&gt; folder is actually a symlink, which links to the built folder in the nix store:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;readlink &lt;/span&gt;result
/nix/store/ccs8597k5ji5h7ad94wfr329xcxydbla-hello-2.12.1
&lt;span class="nv"&gt;$ &lt;/span&gt;tree result/
result/
|-- bin/
    |-- hello
|-- share/
    ...
|-- man/
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This was a super quick introduction to the Nix package manager and nix flakes. Stay tuned for more articles in the series, the next one lined up is about development shells with flakes. You wouldn't want to miss this one!&lt;/p&gt;

&lt;p&gt;If you really liked this article and would like to support me, here are some ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sponsors/arnu515" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://m.do.co/c/371591aa3027" rel="noopener noreferrer"&gt;Use my DigitalOcean referral link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.prisma.io/?via=arnu515" rel="noopener noreferrer"&gt;Use my Prisma referral link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you so much!&lt;/p&gt;

</description>
      <category>nix</category>
      <category>linux</category>
      <category>raspberrypi</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My new Nix series!</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Fri, 27 Dec 2024 09:11:34 +0000</pubDate>
      <link>https://dev.to/arnu515/my-new-nix-series-2cc3</link>
      <guid>https://dev.to/arnu515/my-new-nix-series-2cc3</guid>
      <description>&lt;p&gt;This is a (work-in-progress) series of articles on the &lt;a href="https://nix.dev" rel="noopener noreferrer"&gt;Nix ecosystem&lt;/a&gt;. Some things planned for the near future are development environments with Nix and &lt;a href="https://direnv.net" rel="noopener noreferrer"&gt;direnv&lt;/a&gt;, many &lt;a href="https://nixos.org" rel="noopener noreferrer"&gt;NixOS&lt;/a&gt; related things, and much more. Stay tuned by giving me a follow for when those come out!&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick introduction to Nix
&lt;/h2&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%2F6yw3696omg3z85952w0v.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%2F6yw3696omg3z85952w0v.png" alt="Three " width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://nixos.org" rel="noopener noreferrer"&gt;NixOS website&lt;/a&gt;, Nix is a tool that takes a unique approach to package management and system configuration. This "unique approach" refers to building packages in isolation from each other, ensuring that if packages work on one machine, they will work on another. This is done in a &lt;em&gt;declarative&lt;/em&gt; fashion, essentially meaning that you tell Nix what to do, and it does it for you, without you having to tell Nix &lt;em&gt;how&lt;/em&gt; to do it.&lt;/p&gt;

&lt;p&gt;This is done using the &lt;em&gt;nix&lt;/em&gt; language, which is a &lt;em&gt;purely&lt;/em&gt; functional programming language purpose-built to configure the Nix package manager. The language is simple, dynamically typed, and purely functional. You'll pick it up quite easily, but if you want some formal learning, you can check out the &lt;a href="https://nix.dev/tutorials/nix-language" rel="noopener noreferrer"&gt;Nix Language Basics&lt;/a&gt; chapter of the &lt;a href="https://nix.dev" rel="noopener noreferrer"&gt;Nix documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's a very simple &lt;code&gt;"Hello, world!"&lt;/code&gt; in Nix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="s2"&gt;"Hello, world!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;hello.nix&lt;/code&gt; and run it with the command (if you have Nix installed) &lt;code&gt;nix eval --file hello.nix&lt;/code&gt; to see &lt;code&gt;Hello, world!&lt;/code&gt; output to your terminal!&lt;/p&gt;

&lt;p&gt;This works because every &lt;code&gt;.nix&lt;/code&gt; file contains a nix expression, which are functions, lists, attribute sets (dictionaries), numbers, paths, strings, etc. The &lt;code&gt;nix eval&lt;/code&gt; command evaluates the expression and prints it to the screen.&lt;/p&gt;

&lt;p&gt;The Nix language is used to create packages for the nix ecosystem. Nix packages, as described earlier, are built in isolation from each other, ensuring that there is no &lt;a href="https://en.wikipedia.org/wiki/Dependency_hell" rel="noopener noreferrer"&gt;dependency hell&lt;/a&gt; and allows multiple versions of a package to seamlessly coexist at the same time. Nix packages are &lt;a href="https://nix.dev/manual/nix/2.24/language/derivations" rel="noopener noreferrer"&gt;derivations&lt;/a&gt;: "a specification for running an executable on precisely defined input  files to repeatably produce output files at uniquely determined file  system paths". Nix also provides useful &lt;a href="https://nix.dev/manual/nix/stable/language/builtins.html" rel="noopener noreferrer"&gt;builtin functions&lt;/a&gt; and a &lt;a href="https://nixos.org/manual/nixpkgs/stable/#part-stdenv" rel="noopener noreferrer"&gt;standard environment&lt;/a&gt; to make packaging easier. These packages can then be shared, like any other file, and built by other people without any hassle, since Nix guarantees reproducibility. A collection of 120,000+ packages is available in the &lt;a href="https://github.com/nixos/nixpkgs" rel="noopener noreferrer"&gt;nixpkgs&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Atop all this is the Nix Operating System, or NixOS. NixOS brings all of the declarative goodness of Nix into a Linux distribution. This means that (almost) everything about your system is declarative, including the packages (obviously), the users, the desktop, the login manager, systemd units, containers, and among other things, even the bootloader! Through some extensions, you can also &lt;a href="https://github.com/nix-community/disko" rel="noopener noreferrer"&gt;partition disks&lt;/a&gt;, &lt;a href="https://github.com/nix-community/nixos-generators" rel="noopener noreferrer"&gt;build images&lt;/a&gt;, and even &lt;a href="https://github.com/nix-community/home-manager" rel="noopener noreferrer"&gt;configure your home folder&lt;/a&gt;. A single NixOS configuration is built into multiple pre-configured operating systems using just a single command!&lt;/p&gt;

&lt;h2&gt;
  
  
  Planned Articles
&lt;/h2&gt;

&lt;p&gt;You can see a very crude list of planned articles for this series on &lt;a href="https://gitlab.com/arnu515-tutorials/nix/-/boards/9016066" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. The GitLab repository will also contain any source code and large code snippets (not inlined) used in the series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support me
&lt;/h2&gt;

&lt;p&gt;Here are some ways you can support me on this journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sponsors/arnu515" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://m.do.co/c/371591aa3027" rel="noopener noreferrer"&gt;Use my DigitalOcean referral link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.prisma.io/?via=arnu515" rel="noopener noreferrer"&gt;Use my Prisma referral link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you so much!&lt;/p&gt;

&lt;p&gt;This upcoming series will explore a lot of what Nix can do, and what its &lt;a href="https://github.com/nix-community" rel="noopener noreferrer"&gt;amazing community&lt;/a&gt; has enabled it to do, so stay tuned for more articles in the near future!&lt;/p&gt;

</description>
      <category>nix</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>The one thing I do not like about the Nix package manager (and a fix for it)</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Tue, 16 Jan 2024 16:51:41 +0000</pubDate>
      <link>https://dev.to/arnu515/the-one-thing-i-do-not-like-about-the-nix-package-manager-and-a-fix-for-it-33ln</link>
      <guid>https://dev.to/arnu515/the-one-thing-i-do-not-like-about-the-nix-package-manager-and-a-fix-for-it-33ln</guid>
      <description>&lt;p&gt;The &lt;a href="https://nixos.org"&gt;nix&lt;/a&gt; package manager is an awesome package manager for linux and macos, which focuses on declarative packages. This means that you can dump out all the packages you want into a file, and nix will go out and fetch them for you.&lt;/p&gt;

&lt;p&gt;This package manager builds itself on the concept of reproducibility, and it boasts a collection of over 80,000 packages, second only to the AUR! But this blog post is not about why nix-os is great, you'll find many other &lt;a href="https://serokell.io/blog/what-is-nix"&gt;blog posts&lt;/a&gt; and &lt;a href="https://www.youtube.com/channel/UC_zBdZ0_H_jn41FDRG7q4Tw"&gt;videos&lt;/a&gt; if you want to learn why.&lt;/p&gt;

&lt;p&gt;Many nix package definitions include compiling from source. This can be tedious, since compiling takes a long time and wastes more bandwidth downloading build dependencies. Hence, the nix team has a binary cache, in which they build binaries for all the systems that nix supports for most of the common packages out there, like web browsers, desktop environments, and many more.&lt;/p&gt;

&lt;p&gt;But what if your desired package is not in the binary cache? And what if it takes a long time to compile? This is what happened to me, and this is the one thing I don't like about nix.&lt;/p&gt;

&lt;p&gt;The AUR provides binary packages, alongside source packages, for example, &lt;a href="https://aur.archlinux.org/packages/yay"&gt;yay&lt;/a&gt; and &lt;a href="https://aur.archlinux.org/packages/yay-bin"&gt;yay-bin&lt;/a&gt;, but nix only provides source packages for most of its packages. Granted you'll see some exceptions, like &lt;a href="https://search.nixos.org/packages?channel=23.11&amp;amp;show=firefox&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=relevance&amp;amp;type=packages"&gt;firefox&lt;/a&gt; and &lt;a href="https://search.nixos.org/packages?channel=23.11&amp;amp;show=firefox-bin&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=relevance&amp;amp;type=packages"&gt;firefox-bin&lt;/a&gt;, but those are pretty rare.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;In this article, I'll show you how you can create a binary package for your desired program. I wanted to download the &lt;a href="https://surrealdb.com/"&gt;SurrealDB&lt;/a&gt; package, but the &lt;a href="https://search.nixos.org/packages?channel=unstable&amp;amp;show=surrealdb&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=relevance&amp;amp;type=packages&amp;amp;query=surrealdb"&gt;package on nix&lt;/a&gt; was a source package, meaning that I had to spend over 50 minutes waiting for a stupid package to compile.&lt;/p&gt;

&lt;p&gt;Surreal provides binaries over at its &lt;a href="https://github.com/surrealdb/surrealdb/releases"&gt;GitHub releases&lt;/a&gt;, which I could've downloaded and ran, but I'd have to manually update the package, and as a 10x developer (I'm not one), I'd never manually do anything, but spend 10x the time trying to automate it. Doing this also defeats the reproducibility of nix, hence it's better to create a nix package so that anyone would be able to download your dependencies without having to do anything extra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting set up
&lt;/h2&gt;

&lt;p&gt;First, make sure you have in hand a name for your package, and its version. Generally, binary packages end with &lt;code&gt;-bin&lt;/code&gt;, so I'll call my package &lt;code&gt;surrealdb-bin&lt;/code&gt;, and I'll be downloading the binary for the latest version as of the time of writing, which is &lt;code&gt;v1.1.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since I use NixOS, and since I want to install &lt;code&gt;surrealdb&lt;/code&gt; globally, I'll create my &lt;code&gt;surrealdb&lt;/code&gt; package in &lt;code&gt;/etc/nixos&lt;/code&gt;, which is the folder which holds my &lt;code&gt;configuration.nix&lt;/code&gt;. You may choose to co-locate your package in the directory with your &lt;code&gt;flake.nix&lt;/code&gt;, for example, or even push it to GitHub/Lab so you can make your nix config truly reproducible.&lt;/p&gt;

&lt;p&gt;This approach will contain two &lt;code&gt;nix&lt;/code&gt; files, one for the package itself, and another for an overlay. An &lt;a href="https://nixos.wiki/wiki/Overlays"&gt;overlay&lt;/a&gt; gives you the ability to modify &lt;code&gt;nixpkgs&lt;/code&gt; without having to publish your package to the &lt;code&gt;nixpkgs&lt;/code&gt; repository, and without having to mess around with &lt;code&gt;inputs&lt;/code&gt;. Overlays make it very easy to use your package.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the package
&lt;/h3&gt;

&lt;p&gt;Generally, I put my custom packages in the &lt;code&gt;packages/&lt;/code&gt; subfolder, and custom overlays in the &lt;code&gt;overlays/&lt;/code&gt; subfolder, and I name both these files with the name of the package, in this case &lt;code&gt;surrealdb-bin.nix&lt;/code&gt;. I'd recommend you follow the same strategy to avoid clutter, but if you're only going to create one package, you can just keep everything in one directory.&lt;/p&gt;

&lt;p&gt;I shall refer to &lt;code&gt;packages/surrealdb-bin.nix&lt;/code&gt; as the package declaration. Now, put this code in your package declaration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;stdenv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;fetchzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;autoPatchelfHook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;glibc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gcc-unwrapped&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nv"&gt;stdenv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkDerivation&lt;/span&gt; &lt;span class="kr"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PACKAGE_NAME"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PACKAGE_VERSION"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;fetchzip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PATH_TO_YOUR_PACKAGE'S_TARBALL_HERE"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A_HASH_OF_YOUR_PACKAGE'S_CONTENTS"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;nativeBuildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;autoPatchelfHook&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nv"&gt;buildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nv"&gt;glibc&lt;/span&gt; &lt;span class="nv"&gt;gcc-unwrapped&lt;/span&gt;
    &lt;span class="c"&gt;# Any other system binaries your app may need&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nv"&gt;installPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    runHook preInstall&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    mkdir -p $out/bin&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    install -m755 PACKAGE_BINARY_FILE_NAME $out/bin&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    runHook postInstall&lt;/span&gt;&lt;span class="err"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over this code step-by-step. If you're familiar with nix syntax, you'll recognise that we're creating a function which destructures its first argument to accept some fields, and returns the output of the function &lt;code&gt;stdenv.mkDerivation&lt;/code&gt;. &lt;code&gt;mkDerivation&lt;/code&gt; is the helper function that you use to define &lt;code&gt;nix&lt;/code&gt; packages. We give it an argument which defines the basic metadata of our package. Here are the fields it defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;pname&lt;/code&gt;:&lt;/strong&gt; This is the name of the package. You could also use &lt;code&gt;name&lt;/code&gt;, but you'll have to also specify the package's version in the &lt;code&gt;name&lt;/code&gt;. Using &lt;code&gt;pname&lt;/code&gt; makes nix automatically generate the &lt;code&gt;name&lt;/code&gt; for us.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;version&lt;/code&gt;:&lt;/strong&gt; This is the version of your package. Make sure you're fetching the correct binary!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;src&lt;/code&gt;:&lt;/strong&gt; Now here's the meat of the package. We use nix's &lt;a href="https://ryantm.github.io/nixpkgs/builders/fetchers/"&gt;fetchers&lt;/a&gt; to fetch our package's binary from the internet. There are two mainly used fetchers for binary packages: &lt;code&gt;fetchurl&lt;/code&gt; and &lt;code&gt;fetchzip&lt;/code&gt;. Let's take a closer look at them. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetchurl&lt;/code&gt;:&lt;/strong&gt; This fetcher directly downloads the file provided to it by the &lt;code&gt;url&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetchzip&lt;/code&gt;:&lt;/strong&gt; This file downloads the archive provided to it by the &lt;code&gt;url&lt;/code&gt; field, and unarchives it. The fetcher may be named &lt;code&gt;fetchzip&lt;/code&gt;, but it also works for other archives like &lt;code&gt;.tar.gz&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'll be using the &lt;code&gt;fetchzip&lt;/code&gt; fetcher to download the tarball of the correct SurrealDB version using a direct link pointing to its GitHub release. You can use nix's string interpolation to generate a proper link. This is what mine would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/surrealdb/surrealdb/releases/download/v${version}/surreal-v${version}.linux-amd64.tgz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll replace &lt;code&gt;PATH_TO_YOUR_PACKAGE'S_TARBALL_HERE&lt;/code&gt; with the above link.&lt;/p&gt;

&lt;p&gt;Now for the &lt;code&gt;hash&lt;/code&gt; field. You must specify a checksum hash for the binary that you're downloading so that nix can verify that the correct file is being downloaded. The easiest way to set this value is to set &lt;code&gt;hash&lt;/code&gt; to something random, install the package, and set the hash to whatever it says in the error that gets thrown.&lt;/p&gt;

&lt;p&gt;If you'd like to write &lt;code&gt;hash&lt;/code&gt; yourself, the syntax for it will be:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Many hashing algorithms are supported, but the most commonly used ones are &lt;code&gt;md5&lt;/code&gt; and &lt;code&gt;sha256&lt;/code&gt;. Make sure to &lt;code&gt;base64&lt;/code&gt; encode your hash value, since &lt;code&gt;nix&lt;/code&gt; only accepts that! Don't give it a &lt;code&gt;hex&lt;/code&gt; string.&lt;/p&gt;

&lt;p&gt;Now let's get back to our package derivation. The rest of the code is instructing &lt;code&gt;patchelf&lt;/code&gt; to automatically patch the binary to make it run with nix. Since nix doesn't work like other package managers, binaries which expect shared libraries to be at one particular location will not work, hence we need to use &lt;code&gt;patchelf&lt;/code&gt; to update these locations. Thankfully we won't have to manually run &lt;code&gt;patchelf&lt;/code&gt;, since nix provides us with the &lt;code&gt;autoPatchelf&lt;/code&gt; package. This package is defined in the &lt;code&gt;nativeBuildInputs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The additional libraries which the package depends on must be specified in the &lt;code&gt;buildInputs&lt;/code&gt; array. The best way to find this out, would again be to install the package and add the dependencies it lists out in the error, but generally the two packages I've included should suffice.&lt;/p&gt;

&lt;p&gt;Finally, in the &lt;code&gt;installPhase&lt;/code&gt;, we define a shell script that runs. The &lt;code&gt;hooks&lt;/code&gt; that are called are all related to &lt;code&gt;patchelf&lt;/code&gt;, so just make sure they're present in your code. In between the two hook calls, we write code to create a &lt;code&gt;bin/&lt;/code&gt; directory in our package's nix-store path, and we transfer any binaries the package provides to the &lt;code&gt;bin/&lt;/code&gt; folder. Do update the &lt;code&gt;PACKAGE_BINARY_FILE_NAME&lt;/code&gt; variable with the name of the binary that gets downloaded by the fetcher. In my case, that'd be &lt;code&gt;surreal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;install&lt;/code&gt; command is actually just some syntactic sugar for the &lt;code&gt;cp&lt;/code&gt; command, they both have the same function. The only extra thing is the &lt;code&gt;-m&lt;/code&gt; flag, which sets &lt;code&gt;chmod&lt;/code&gt; permissions on the binary to make it readable, writable and executable by the user (the &lt;code&gt;7&lt;/code&gt;), and only readable and executable by the user's group and everyone else (the two &lt;code&gt;5&lt;/code&gt;s).&lt;/p&gt;

&lt;p&gt;Finally, this is how your package derivation should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/nixos/packages/surrealdb-bin.nix &lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;stdenv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;fetchzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;autoPatchelfHook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;glibc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gcc-unwrapped&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nv"&gt;stdenv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkDerivation&lt;/span&gt; &lt;span class="kr"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"surrealdb-bin"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.1.0"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;fetchzip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/surrealdb/surrealdb/releases/download/v&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/surreal-v&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.linux-amd64.tgz"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2611de5eb7779dfe3b32bb47833fee2e3e168e39e43d76b47ea649b2f8c407fa"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;nativeBuildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;autoPatchelfHook&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nv"&gt;buildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;glibc&lt;/span&gt; &lt;span class="nv"&gt;gcc-unwrapped&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nv"&gt;installPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    runHook preInstall&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    mkdir -p $out/bin&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    install -m755 surreal $out/bin&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    runHook postInstall&lt;/span&gt;&lt;span class="err"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating the overlay
&lt;/h3&gt;

&lt;p&gt;Now we need to create an overlay to be able to add our package to the existing list of &lt;code&gt;nixpkgs&lt;/code&gt;. I'll call my overlay derivation &lt;code&gt;surrealdb-bin.nix&lt;/code&gt;, and place it in the &lt;code&gt;overlays/&lt;/code&gt; folder in the &lt;code&gt;/ext/nixos&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;If you're using a different path than me, be sure to update the relevant imports.&lt;/p&gt;

&lt;p&gt;Add these lines to your overlay definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;super&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;surrealdb-bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;callPackage&lt;/span&gt; &lt;span class="sx"&gt;../packages/surrealdb-bin.nix&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;Now this may seem a little scarier, if you're new to the &lt;code&gt;nix&lt;/code&gt; language. Since nix functions can only accept one argument, we use nested functions to declare multiple arguments. &lt;code&gt;self&lt;/code&gt; and &lt;code&gt;super&lt;/code&gt;, as they're commonly called, or more recently &lt;code&gt;final&lt;/code&gt; and &lt;code&gt;prev&lt;/code&gt;, are both instances of &lt;code&gt;nixpkgs&lt;/code&gt;. It's just that &lt;code&gt;super&lt;/code&gt;/&lt;code&gt;prev&lt;/code&gt; is the version of &lt;code&gt;nixpkgs&lt;/code&gt; before the overlay is applied, and &lt;code&gt;self&lt;/code&gt;/&lt;code&gt;final&lt;/code&gt; is the version of &lt;code&gt;nixpkgs&lt;/code&gt; after all overlays are applied.&lt;/p&gt;

&lt;p&gt;You should generally only use the &lt;code&gt;super&lt;/code&gt; argument. Read &lt;a href="https://nixos.wiki/wiki/Overlays"&gt;the wiki&lt;/a&gt; if you want to learn more about overlays.&lt;/p&gt;

&lt;p&gt;The overlay functions return type is a set of packages which will be merged into &lt;code&gt;nixpkgs&lt;/code&gt;. Here you can see that I'm defining one package called &lt;code&gt;surrealdb-bin&lt;/code&gt;, and I'm calling the &lt;code&gt;super.callPackage&lt;/code&gt; function and giving it the location of my &lt;code&gt;surrealdb-bin&lt;/code&gt; package derivation as the argument. The &lt;code&gt;callPackage&lt;/code&gt; function ensures that the package derivation is called with the proper arguments supplied. The blank argument set at the end is just to specify that I don't want to extend the list of arguments passed to the package derivation any further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the overlay
&lt;/h3&gt;

&lt;p&gt;The only thing left to do now, is to actually use the overlay and extend the &lt;code&gt;nixpkgs&lt;/code&gt; definition. Now this depends on where you plan to use the overlay, i.e. in a &lt;code&gt;shell.nix&lt;/code&gt;, &lt;code&gt;flake.nix&lt;/code&gt;, the nixos &lt;code&gt;configuration.nix&lt;/code&gt;, or in a home-manager configuration. The steps are different for all of those.&lt;/p&gt;

&lt;p&gt;Check &lt;a href="https://nixos.wiki/wiki/Overlays#Applying_overlays_manually"&gt;the wiki&lt;/a&gt; for instructions pertaining to your specific use case. Since I want to install &lt;code&gt;surreal&lt;/code&gt; globally, I'll add the overlay to my &lt;code&gt;configuration.nix&lt;/code&gt;. All I have to do is add the below line somewhere, and then add &lt;code&gt;surrealdb-bin&lt;/code&gt; to the list of environment/user packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;overlays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="sx"&gt;./overlays/surrealdb-bin.nix&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;blockquote&gt;
&lt;p&gt;Don't forget to update the path to the overlay if you're using a different path!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And with a small &lt;code&gt;nixos-rebuild switch&lt;/code&gt;, I have access to the &lt;code&gt;surreal&lt;/code&gt; command in my environment! Great success! Now if there's a new surreal version published, all I have to do is change the &lt;code&gt;version&lt;/code&gt; in my package derivation and rebuild!&lt;/p&gt;

&lt;p&gt;You should push your package derivation and overlay to a centralised place like GitHub, GitLab, or any other place that provides direct download links (GitHub Gist / GitLab snippets would be a great place), so that you can download this overlay in any nix configuration and maintain reproducibility, instead of possibly having to duplicate your overlay and maintain two sources of truths.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if I don't have access to binaries
&lt;/h2&gt;

&lt;p&gt;If your package only has the source available, with no binaries, and still takes a long time to compile, then you should probably leave the compilation to a GitHub action.&lt;/p&gt;

&lt;p&gt;Just create a GitHub repository, create an action to build the package, and then upload it as an artifact/release once done.&lt;/p&gt;

&lt;p&gt;Then use Nix to fetch that artifact in your package.&lt;/p&gt;

&lt;p&gt;Do note that GitHub-provided hosted action runners have a maximum runtime of 6 hours, so if you have a package that takes longer than that (e.g. a web browser), then you need to use self-hosted runners, which have a max runtime of 35 days!&lt;/p&gt;

&lt;p&gt;It is risky to host a self-hosted runner on your own machine, since your machine may go down, and you'll have to restart the build process again&lt;/p&gt;

&lt;p&gt;Instead it'd be best to use a cloud VPS service like &lt;a href="https://m.do.co/c/371591aa3027"&gt;Digital Ocean&lt;/a&gt;. If you use that link, or &lt;a href="https://m.do.co/c/371591aa3027"&gt;this one&lt;/a&gt;, you can get &lt;code&gt;$200&lt;/code&gt; in credit for upto 60 days, that's literally giving you free access to a 16GiB ram + 8 vCPU instance to build all your hopes and dreams on! (You will have to request access to these machines first though).&lt;/p&gt;

&lt;p&gt;If you use &lt;a href="https://m.do.co/c/371591aa3027"&gt;my link&lt;/a&gt;, I also get some credits, so it helps me out too! Thanks for using &lt;a href="https://m.do.co/c/371591aa3027"&gt;my link&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Anyway, custom GitHub actions are out of scope of this article, but do let me know if you want me to create another article on that topic in the comments below!&lt;/p&gt;

</description>
      <category>nix</category>
      <category>linux</category>
      <category>tutorial</category>
      <category>packages</category>
    </item>
    <item>
      <title>Create a Reddit clone with RedwoodJS</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Sun, 20 Feb 2022 14:41:46 +0000</pubDate>
      <link>https://dev.to/arnu515/create-a-reddit-clone-with-redwoodjs-pcp</link>
      <guid>https://dev.to/arnu515/create-a-reddit-clone-with-redwoodjs-pcp</guid>
      <description>&lt;p&gt;Redwood is an opinionated full-stack javascript web application framework. It is also serverless-ready, meaning it can be deployed &lt;em&gt;on the edge&lt;/em&gt; with services like AWS Lambda and Cloudflare Workers. Redwood is &lt;em&gt;super&lt;/em&gt; opinionated. It decides your project/directory structure, it decides the frameworks and libraries you use, and it configures everything for you. Some may see a downside to this, but if you're experienced with Redwood's choosing of frameworks, you will have a pleasant time using Redwood.&lt;/p&gt;

&lt;p&gt;Redwood was created by &lt;a href="https://github.com/mojombo" rel="noopener noreferrer"&gt;Tom Preston-Werner&lt;/a&gt;. You may have heard of him before, because he is the guy behind&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com" rel="noopener noreferrer"&gt;Github&lt;/a&gt;, which is the most popular code host&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jekyllrb.com" rel="noopener noreferrer"&gt;Jekyll&lt;/a&gt;, a ruby-based static-site generator&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gravatar.com" rel="noopener noreferrer"&gt;Gravatar&lt;/a&gt;, a very popular avatar service&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://semver.org" rel="noopener noreferrer"&gt;Semver&lt;/a&gt;, the semantic versioning system&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://toml.io" rel="noopener noreferrer"&gt;TOML&lt;/a&gt;, a configuration language, like JSON or YAML, and much more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redwood uses &lt;a href="https://reactjs.org" rel="noopener noreferrer"&gt;React&lt;/a&gt; for the frontend framework, so you'll need to know React.&lt;/li&gt;
&lt;li&gt;Redwood uses &lt;a href="https://graphql.org" rel="noopener noreferrer"&gt;GraphQL&lt;/a&gt; instead of REST APIs, so knowledge of that is &lt;strong&gt;required&lt;/strong&gt;. You can learn it on the &lt;a href="https://graphql.org/learn/" rel="noopener noreferrer"&gt;official website&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Redwood uses &lt;a href="https://prisma.io" rel="noopener noreferrer"&gt;Prisma&lt;/a&gt; for interacting with databases, but it's very easy to use, and you can pick it up from this tutorial itself. Prisma works with SQL databases.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://postgresql.org" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; database running. You can either have the Postgres server installed, or use &lt;a href="https://docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;. I'll be doing the latter in this tutorial.&lt;/li&gt;
&lt;li&gt;There are various other libraries used like &lt;a href="https://jestjs.io" rel="noopener noreferrer"&gt;Jest&lt;/a&gt; and &lt;a href="https://storybook.js.org" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;, but these are not needed to follow this tutorial.&lt;/li&gt;
&lt;li&gt;I'll be using &lt;a href="https://typescriptlang.org" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt; in this tutorial, but feel free to use plain JavaScript. Just be sure to remove any code that is TypeScript-specific.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also a few things you'll need installed on your computer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;NodeJS&lt;/a&gt; v14 or higher. I'll be using v16. (Psst: For an easy way to manage versions of NodeJS and many others, try &lt;a href="https://asdf-vm.com" rel="noopener noreferrer"&gt;https://asdf-vm.com&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://yarnpkg.com" rel="noopener noreferrer"&gt;Yarn&lt;/a&gt; Package Manager installed. Redwood leverages yarn workspaces, so yarn is needed. You can install it using &lt;code&gt;npm i -g yarn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A powerful code editor like &lt;a href="https://code.visualstudio.com" rel="noopener noreferrer"&gt;VSCode&lt;/a&gt; or (Neo)Vim. If you're using VSCode, be sure to install the Redwood extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 0 — Creating your Redwood app
&lt;/h2&gt;

&lt;p&gt;Open an empty folder in your favorite IDE and run the below command in the terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn create redwood-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're like me however, and you've fallen in love with &lt;a href="https://typescriptlang.org" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt;, you can create a Redwood typescript app by adding the &lt;code&gt;--typescript&lt;/code&gt; flag to the above command like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn create &lt;span class="nt"&gt;--typescript&lt;/span&gt; redwood-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you want to convert an existing Redwood project to TypeScript, you can run &lt;code&gt;yarn rw setup tsconfig&lt;/code&gt; and change your &lt;code&gt;.js&lt;/code&gt; files to &lt;code&gt;.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now open the project in your favorite IDE. I'll use VSCode in this tutorial, since Redwood has first-class support for it. Launch the editor and open the folder, or just run &lt;code&gt;code .&lt;/code&gt; in the terminal.&lt;/p&gt;

&lt;p&gt;You may be prompted to install recommended extensions, so feel free to install them all, or just some if you don't need certain extensions (like Gitlens, in my case).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Getting to know your project
&lt;/h2&gt;

&lt;p&gt;Let's take a look at the project structure.&lt;/p&gt;

&lt;p&gt;There are a few files in the root project. Most of them are configuration files, like &lt;code&gt;jest.config.js&lt;/code&gt;. Let's take a look at a specific file called &lt;code&gt;redwood.toml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[web]&lt;/span&gt;
  &lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Redwood App"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8910&lt;/span&gt;
  &lt;span class="py"&gt;apiUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/.redwood/functions"&lt;/span&gt; &lt;span class="c"&gt;# you can customise graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths&lt;/span&gt;
  &lt;span class="py"&gt;includeEnvironmentVariables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="c"&gt;# any ENV vars that should be available to the web side, see https://redwoodjs.com/docs/environment-variables#web&lt;/span&gt;
&lt;span class="nn"&gt;[api]&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8911&lt;/span&gt;
&lt;span class="nn"&gt;[browser]&lt;/span&gt;
  &lt;span class="py"&gt;open&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Redwood recommends so many extensions, but not a TOML one! Install &lt;a href="https://marketplace.visualstudio.com/items?itemName=bungcip.better-toml" rel="noopener noreferrer"&gt;this extension&lt;/a&gt; for VSCode for TOML highlighting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're unfamiliar with TOML syntax, don't worry, I'll guide you through the config. For now, let's change the &lt;code&gt;port&lt;/code&gt; of both the &lt;code&gt;[web]&lt;/code&gt; and &lt;code&gt;[api]&lt;/code&gt; projects to &lt;code&gt;3000&lt;/code&gt; and &lt;code&gt;5000&lt;/code&gt; respectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where your code lives
&lt;/h3&gt;

&lt;p&gt;There are many directories, but the main two directories are &lt;code&gt;web&lt;/code&gt;, and &lt;code&gt;api&lt;/code&gt;. This is where your Redwood code lives. As the name suggests, &lt;code&gt;web&lt;/code&gt; is the frontend project, and &lt;code&gt;api&lt;/code&gt; is the backend project.&lt;/p&gt;

&lt;p&gt;These two directories are actually their own projects, and Redwood uses Yarn Workspaces to link these two folders together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside the &lt;code&gt;web&lt;/code&gt; folder&lt;/strong&gt;:&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%2Fi.imgur.com%2FAS7I7Lc.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%2Fi.imgur.com%2FAS7I7Lc.png" alt="Screenshot of the subfolders inside the web folder"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;web&lt;/code&gt; folder is a regular ol' React application. If you know react, you should be able to read through the structure of this directory. There are just a few things that are different. In the &lt;code&gt;src&lt;/code&gt; folder, you can see three more subfolders, &lt;code&gt;components&lt;/code&gt;, &lt;code&gt;layouts&lt;/code&gt; and &lt;code&gt;pages&lt;/code&gt;. The &lt;code&gt;components&lt;/code&gt; folder holds any re-usable React components. The &lt;code&gt;layouts&lt;/code&gt; folder holds page layouts, which are also React components, and the &lt;code&gt;pages&lt;/code&gt; folder, which contains React components mapped to routes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a &lt;code&gt;.keep&lt;/code&gt; file?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;.keep&lt;/code&gt; files are just files that are placed in empty directories so they get committed to a git project. Git doesn't commit empty folders, so &lt;code&gt;.keep&lt;/code&gt; files are created to make the folder &lt;em&gt;not empty&lt;/em&gt;, and hence, get it committed. These &lt;code&gt;.keep&lt;/code&gt; files don't even have to be called &lt;code&gt;.keep&lt;/code&gt;, they can be called anything else, but by convention, they're called &lt;code&gt;.keep&lt;/code&gt; and &lt;code&gt;.gitkeep&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Inside the &lt;code&gt;api&lt;/code&gt; folder&lt;/strong&gt;:&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%2Fi.imgur.com%2F1h7HAko.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%2Fi.imgur.com%2F1h7HAko.png" alt="Screenshot of the folders inside the api folder"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;api&lt;/code&gt; folder is the backend server. This is running &lt;code&gt;fastify&lt;/code&gt; under the hood, which is just a faster backend server than express. There are a few config files, and there are three subdirectories.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;db&lt;/code&gt; folder contains the &lt;code&gt;schema.prisma&lt;/code&gt; file, which is the schema for your database models that is used by Prisma ORM.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;src&lt;/code&gt; folder contains all of your source code for the backend.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;From the &lt;a href="https://learn.redwoodjs.com/docs/tutorial/redwood-file-structure" rel="noopener noreferrer"&gt;redwood documentation&lt;/a&gt;: &lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;directives&lt;/code&gt; will contain GraphQL &lt;a href="https://www.graphql-tools.com/docs/schema-directives" rel="noopener noreferrer"&gt;schema directives&lt;/a&gt; for controlling access to queries and transforming values.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;functions&lt;/code&gt; will contain any &lt;a href="https://docs.netlify.com/functions/overview/" rel="noopener noreferrer"&gt;lambda functions&lt;/a&gt; your app needs in addition to the &lt;code&gt;graphql.js&lt;/code&gt; file auto-generated by Redwood. This file is required to use the GraphQL API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;graphql&lt;/code&gt; contains your GraphQL schema written in a Schema Definition Language (the files will end in &lt;code&gt;.sdl.js&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib&lt;/code&gt; contains a few files:&lt;code&gt;auth.js&lt;/code&gt; starts as a placeholder for adding auth functionality and has a couple of bare-bones functions in it to start, &lt;code&gt;db.js&lt;/code&gt; instantiates the Prisma database client so we can talk to a database and &lt;code&gt;logger.js&lt;/code&gt; which configures, well, logging. You can use this directory for other code related to the API side that doesn't really belong anywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;services&lt;/code&gt; contains business logic related to your data. When you're querying or mutating data for GraphQL (known as &lt;strong&gt;resolvers&lt;/strong&gt;), that code ends up here, but in a format that's reusable in other places in your application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Start the server&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Let's start the server by running the below command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn rw dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now see your application running on &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;, or &lt;a href="http://localhost:8911" rel="noopener noreferrer"&gt;http://localhost:8911&lt;/a&gt;, if you didn't change the port in the config. The backend will run on port &lt;code&gt;5000&lt;/code&gt;, or &lt;code&gt;8910&lt;/code&gt; if you didn't change the port in the config.&lt;/p&gt;

&lt;p&gt;If this is what you see, you've successfully created your redwood project!&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%2Fi.imgur.com%2F1iBwiDg.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%2Fi.imgur.com%2F1iBwiDg.png" alt="Screenshot of localhost:3000"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Replacing SQLLite with Postgres&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;SQLLite is the default database used by Redwood, since it doesn't expect everyone to have a fully-fledged database installed and running on their computer. But SQLLite is a file-system based database, and it also lacks in features when compared to Postgres. A file-system based database isn't the best for production, so let's switch over to Postgres.&lt;/p&gt;

&lt;p&gt;Postgres needs to be installed on your computer. You can download it and install it, and have a system-wide install of postgres, or you can use &lt;a href="https://docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; to &lt;em&gt;containerize&lt;/em&gt; it, which is easier to do. You'll need docker installed, however, and you can get it from &lt;a href="https://www.docker.com/get-started" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once docker is running, you can create a &lt;code&gt;postgres&lt;/code&gt; container using the command below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5432:5432 &lt;span class="nt"&gt;--name&lt;/span&gt; postgres &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_STRONG_PASSWORD postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Be sure to replace &lt;code&gt;YOUR_STRONG_PASSWORD&lt;/code&gt; to a strong password, since that will be the password of your root account in postgres.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The above command will run the &lt;a href="https://hub.docker.com/_/postgres" rel="noopener noreferrer"&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/a&gt; image as a container, with the name &lt;code&gt;postgres&lt;/code&gt; (with the &lt;code&gt;--name&lt;/code&gt; flag), adds the environment variable &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; to it (with the &lt;code&gt;-e&lt;/code&gt; flag), exposes port &lt;code&gt;5432&lt;/code&gt; (postgres' default port) back to the host (with the &lt;code&gt;-p&lt;/code&gt; flag) and finally, it runs it in the background with the &lt;code&gt;-d&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;Now, create a new database in the fresh postgres container you just created. Run the below command to get &lt;em&gt;shell&lt;/em&gt; access to the container:&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; postgres bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your &lt;em&gt;shell prompt&lt;/em&gt; changed, you now have the ability to run commands directly in the postgres container! Now run the below command to create a new database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;createdb &lt;span class="nt"&gt;-U&lt;/span&gt; postgres NAME_OF_YOUR_DATABASE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-U postgres&lt;/code&gt; flag makes it run as the &lt;code&gt;postgres&lt;/code&gt; user, which is the default root user. Change &lt;code&gt;NAME_OF_YOUR_DATABASE&lt;/code&gt; to anything you want. In my case, I changed it to &lt;code&gt;reddit&lt;/code&gt;, which means that a new database with the name &lt;code&gt;reddit&lt;/code&gt; has been created for me. Once that's done, exit out of the shell by typing &lt;code&gt;exit&lt;/code&gt; and hitting Enter.&lt;/p&gt;

&lt;p&gt;Now that you have a postgres database, you just need to tell Prisma to use it. Open the &lt;code&gt;.env&lt;/code&gt; file in the project root and add the below code to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=postgres://postgres:YOUR_STRONG_PASSWORD@localhost:5432/YOUR_DATABASE_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be sure to replace &lt;code&gt;YOUR_STRONG_PASSWORD&lt;/code&gt; and &lt;code&gt;YOUR_DATABASE_NAME&lt;/code&gt; with the relevant values. And finally, change the line that says &lt;code&gt;provider = "sqlite"&lt;/code&gt; to &lt;code&gt;provider = "postgresql"&lt;/code&gt; in the &lt;code&gt;api/db/schema.prisma&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;datasource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;db&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 — Creating Prisma models
&lt;/h2&gt;

&lt;p&gt;Prisma models are definitions for how your database tables will look like. They are written in prisma's own model language in the &lt;code&gt;schema.prisma&lt;/code&gt; file. If you're not familiar with this syntax, don't fear, since it looks similar to GraphQL syntax, and I'll guide you with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the User model
&lt;/h3&gt;

&lt;p&gt;Open the &lt;code&gt;src/db/schema.prisma&lt;/code&gt; file in the &lt;code&gt;api&lt;/code&gt; project. Let's delete the example &lt;code&gt;UserExample&lt;/code&gt; project, and replace it with our own User model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;avatarUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bio&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;isBanned&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;is_banned&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you can't see any syntax highlighting, be sure to install the Prisma extension for VSCode.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What the above Prisma code does, is it creates a model named &lt;code&gt;User&lt;/code&gt;. A Prisma model is mapped to a table in the database, which in this case will be &lt;code&gt;users&lt;/code&gt;, because of the &lt;code&gt;@@map("users")&lt;/code&gt;. These are the fields that will be created:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;id&lt;/code&gt; filed, which will be the primary key (denoted by &lt;code&gt;@id&lt;/code&gt;). It will be a String with the &lt;code&gt;VarChar&lt;/code&gt; datatype in Postgres. Since &lt;code&gt;VarChar&lt;/code&gt; isn't supported by all databases Prisma supports (like MongoDB), we have to use &lt;code&gt;@db.VarChar&lt;/code&gt; instead of directly declaring it as a &lt;code&gt;VarChar&lt;/code&gt; type. The &lt;code&gt;id&lt;/code&gt; will also be a generated &lt;code&gt;CUID&lt;/code&gt; by default. A CUID is a randomly-generated string, like a UUID.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;username&lt;/code&gt; and an &lt;code&gt;email&lt;/code&gt; field, both of which are &lt;code&gt;String&lt;/code&gt;s and are &lt;code&gt;unique&lt;/code&gt;, meaning no two users can have the same &lt;code&gt;email&lt;/code&gt; or &lt;code&gt;username&lt;/code&gt;. By default, a &lt;code&gt;String&lt;/code&gt; will be mapped to Postgres' &lt;code&gt;Text&lt;/code&gt; datatype.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;password&lt;/code&gt;, which is a &lt;code&gt;String&lt;/code&gt; in Prisma, but a &lt;code&gt;VarChar&lt;/code&gt; in Postgres&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;avatarUrl&lt;/code&gt;, which is a &lt;code&gt;String&lt;/code&gt;. This will be accessed in JavaScript with &lt;code&gt;avatarUrl&lt;/code&gt;, but will be stored in the database as &lt;code&gt;avatar_url&lt;/code&gt;, because of &lt;code&gt;@map&lt;/code&gt;. I did this because Postgres follows &lt;code&gt;snake_casing&lt;/code&gt;, while JavaScript follows &lt;code&gt;camelCasing&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;roles&lt;/code&gt;, which is a &lt;code&gt;String&lt;/code&gt;, which will contain a comma-separated string of roles. You could use an array here, but I feel like that would be overkill for a field that would usually only have one role. Also &lt;code&gt;member&lt;/code&gt; is the default.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;bio&lt;/code&gt;, which is an &lt;em&gt;optional&lt;/em&gt; string (&lt;code&gt;nullable&lt;/code&gt;, in database lingo). This is indicated by the &lt;code&gt;?&lt;/code&gt; after &lt;code&gt;String&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;And finally, &lt;code&gt;isBanned&lt;/code&gt;, which is a &lt;code&gt;Boolean&lt;/code&gt; that defaults to &lt;code&gt;false&lt;/code&gt;, and is stored as &lt;code&gt;is_banned&lt;/code&gt; in the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you need to sync the models to your database. Currently, they're only present in the &lt;code&gt;schema.prisma&lt;/code&gt; file. To create the tables in the database, run the below command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn redwood prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You can use the alias &lt;code&gt;rw&lt;/code&gt; instead of &lt;code&gt;redwood&lt;/code&gt;, and that's what I'll be doing from now on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Be sure to give it a meaningful name. Treat it like a git commit – the name should reflect the changes you've made. In this case, we've created a &lt;code&gt;User&lt;/code&gt; model, so I named it &lt;code&gt;add-user-model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now your database will have a table called &lt;code&gt;users&lt;/code&gt; with all these fields that you just defined.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Post model
&lt;/h3&gt;

&lt;p&gt;Now it's time to create a model for holding our posts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasMedia&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;has_media&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;mediaUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You may see a squiggly line under the &lt;code&gt;author&lt;/code&gt; field. Don't worry, we'll solve that soon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fields in this model are pretty similar to the ones in the &lt;code&gt;User&lt;/code&gt; model, except they have different names. There is one odd one out however, and that is &lt;code&gt;authorId&lt;/code&gt;. This &lt;code&gt;authorId&lt;/code&gt; field will point to the &lt;code&gt;id&lt;/code&gt; of the &lt;code&gt;User&lt;/code&gt; that created this post, and this is denoted by the &lt;code&gt;author User&lt;/code&gt; line. It has an &lt;code&gt;@relation&lt;/code&gt; directive that relates the &lt;code&gt;id&lt;/code&gt; field of &lt;code&gt;User&lt;/code&gt; to the &lt;code&gt;authorId&lt;/code&gt; field of &lt;code&gt;Post&lt;/code&gt;. Prisma also requires that we include a backref – a field on the other table that points back to this one indicating the relation. Since this will be a one-to-many (O2M) relation, i.e. one user can have many posts, the post backref in the User model should be an array. You can denote that by putting square brackets (&lt;code&gt;[]&lt;/code&gt;) after the type, just like in regular TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;avatarUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bio&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;isBanned&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;is_banned&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="err"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;backref&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasMedia&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;has_media&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;mediaUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While we're at it, let's also add a &lt;code&gt;Comment&lt;/code&gt; model, which will store comments on a post. This model will have two relations — both O2M — one with the &lt;code&gt;User&lt;/code&gt; model, and the other with the &lt;code&gt;Post&lt;/code&gt; model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;avatarUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bio&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;isBanned&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;is_banned&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="err"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;backref&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Comment&lt;/span&gt;&lt;span class="err"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;backref&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Comment&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasMedia&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;has_media&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;mediaUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Comment&lt;/span&gt;&lt;span class="err"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;backref&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Comment&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Our&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;@relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;@relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should notice that the fields that are in a relation have the same type as the field they're in a relationship with. This is important, since they'll be storing the same type of data.&lt;/p&gt;

&lt;p&gt;Let's migrate our database! Run the same command as before, and this time, you can give it a name directly in the command line with the &lt;code&gt;--name&lt;/code&gt; argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn rw prisma migrate dev &lt;span class="nt"&gt;--name&lt;/span&gt; add-post-and-comment-models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, our three basic models have been created. Let's now use them in the Redwood project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Adding authentication to your app
&lt;/h2&gt;

&lt;p&gt;Redwood makes it really easy to add authentication to your application. It handles almost everything that is boring, like sessions and stuff like that.&lt;/p&gt;

&lt;p&gt;Let's use the Redwood CLI and sets up authentication for you. Run the below command to do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn rw setup auth dbAuth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will setup a local authentication provider that saves your users' credentials on the database. Redwood also supports some authentication-as-a-service providers out-of-the-box, like &lt;a href="https://auth0.com" rel="noopener noreferrer"&gt;Auth0&lt;/a&gt; and &lt;a href="https://magic.link" rel="noopener noreferrer"&gt;Magic&lt;/a&gt;. Read more about that &lt;a href="https://redwoodjs.com/docs/authentication" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A few new files have been created. You need to edit these files to make it work with your schema. First, let's edit &lt;code&gt;src/lib/auth.ts&lt;/code&gt;. This file contains methods that are used by Redwood under-the-hood to determine if a user is authenticated and authorized to access a resource.&lt;/p&gt;

&lt;p&gt;You only need to do one small edit – make Redwood read the roles stored in the &lt;code&gt;users&lt;/code&gt; table in the &lt;code&gt;hasRole&lt;/code&gt; function. But first. let's make the &lt;code&gt;getCurrentUser&lt;/code&gt; function return the whole user, instead of just the user's &lt;code&gt;id&lt;/code&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getCurrentUser&lt;/span&gt; &lt;span class="o"&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;session&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;session&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="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;In the above snippet, I just removed the &lt;code&gt;select {...}&lt;/code&gt; from the query so it returns all fields of the user. We can now use this in the &lt;code&gt;hasRole&lt;/code&gt; function. Change out the &lt;code&gt;hasRole&lt;/code&gt; function to the one below:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AllowedRoles&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isAuthenticated&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="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// If your User model includes roles, uncomment the role checks on currentUser&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// the line below has changed&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&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="s1"&gt;,&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="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;role&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;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;))&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&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="c1"&gt;// the line below has changed&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&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="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;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// roles not found&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code should now check the roles in the database instead of returning false by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding some fields to the &lt;code&gt;User&lt;/code&gt; model&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Redwood gives you a &lt;code&gt;hashedPassword&lt;/code&gt;, a &lt;code&gt;salt&lt;/code&gt;, a &lt;code&gt;resetToken&lt;/code&gt; and a &lt;code&gt;resetTokenExpiresAt&lt;/code&gt; to store in your database, but the current &lt;code&gt;User&lt;/code&gt; model can only store the password. Let's change that by adding three new fields to the &lt;code&gt;User&lt;/code&gt; model by changing the &lt;code&gt;User&lt;/code&gt; model in &lt;code&gt;schema.prisma&lt;/code&gt; to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cuid&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@unique&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;below&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;three&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;resetToken&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VarChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;resetTokenExp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@db&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamptz&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;avatarUrl&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bio&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;isBanned&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;@default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;is_banned&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="err"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Comment&lt;/span&gt;&lt;span class="err"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you were messing around and created a few users, your migration will fail because &lt;code&gt;salt&lt;/code&gt; is empty, and &lt;code&gt;salt&lt;/code&gt; is not allowed to be empty. So, just add &lt;code&gt;@default("")&lt;/code&gt; to the &lt;code&gt;salt&lt;/code&gt; field in the schema to ensure that already-existing users won't have &lt;code&gt;null&lt;/code&gt; values for required fields.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, migrate with the below command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn rw prisma migrate dev &lt;span class="nt"&gt;--name&lt;/span&gt; add-fields-to-user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Of course, you can use your own migration name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, you'll need to generate types so Redwood knows about the new User.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn rw generate types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, you need to restart the dev server. Press &lt;code&gt;Ctrl+C&lt;/code&gt; (maybe twice) to stop the current running dev server and run &lt;code&gt;yarn rw dev&lt;/code&gt; to start it again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuring authentication&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;There are just a few final changes you need to make to the &lt;code&gt;src/functions/auth.ts&lt;/code&gt; file, such as setting an &lt;code&gt;avatarUrl&lt;/code&gt;. For the &lt;code&gt;avatarUrl&lt;/code&gt;, we'll use &lt;a href="https://gravatar.com" rel="noopener noreferrer"&gt;Gravatar&lt;/a&gt;, which is a popular avatar service. For that, you just need to use the below URL as the &lt;code&gt;avatarUrl&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://gravatar.com/avatar/EMAIL_HASH?d=mp&amp;amp;s=64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;EMAIL_HASH&lt;/code&gt; should be an &lt;code&gt;md5&lt;/code&gt; hash of the user's email. For generating an &lt;code&gt;md5&lt;/code&gt; hash, let's install the &lt;code&gt;md5&lt;/code&gt; package (along with its typescript definitions) with the below commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn workspace api add md5 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; yarn workspace api add &lt;span class="nt"&gt;-D&lt;/span&gt; @types/md5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;We're using &lt;code&gt;workspace api add&lt;/code&gt; instead of just &lt;code&gt;add&lt;/code&gt; because there are two workspaces here, and we just want to add &lt;code&gt;md5&lt;/code&gt; to the &lt;code&gt;api&lt;/code&gt; folder, not the &lt;code&gt;web&lt;/code&gt; folder.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, let's change the &lt;code&gt;src/functions/auth.ts&lt;/code&gt; file to make sure it works with our requirements:&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;db&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;src/lib/db&lt;/span&gt;&lt;span class="dl"&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;DbAuthHandler&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;@redwoodjs/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;md5&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;md5&lt;/span&gt;&lt;span class="dl"&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;handler&lt;/span&gt; &lt;span class="o"&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;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;forgotPasswordOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// handler() is invoked after verifying that a user was found with the given&lt;/span&gt;
    &lt;span class="c1"&gt;// username. This is where you can send the user an email with a link to&lt;/span&gt;
    &lt;span class="c1"&gt;// reset their password. With the default dbAuth routes and field names, the&lt;/span&gt;
    &lt;span class="c1"&gt;// URL to reset the password will be:&lt;/span&gt;
    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// https://example.com/reset-password?resetToken=${user.resetToken}&lt;/span&gt;
    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// Whatever is returned from this function will be returned from&lt;/span&gt;
    &lt;span class="c1"&gt;// the `forgotPassword()` function that is destructured from `useAuth()`&lt;/span&gt;
    &lt;span class="c1"&gt;// You could use this return value to, for example, show the email&lt;/span&gt;
    &lt;span class="c1"&gt;// address in a toast message so the user will know it worked and where&lt;/span&gt;
    &lt;span class="c1"&gt;// to look for the email.&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="nx"&gt;user&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// How long the resetToken is valid for, in seconds (default is 24 hours)&lt;/span&gt;
    &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// for security reasons you may want to be vague here rather than expose&lt;/span&gt;
      &lt;span class="c1"&gt;// the fact that the email address wasn't found (prevents fishing for&lt;/span&gt;
      &lt;span class="c1"&gt;// valid email addresses)&lt;/span&gt;
      &lt;span class="na"&gt;usernameNotFound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// if the user somehow gets around client validation&lt;/span&gt;
      &lt;span class="na"&gt;usernameRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is required&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loginOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// handler() is called after finding the user that matches the&lt;/span&gt;
    &lt;span class="c1"&gt;// username/password provided at login, but before actually considering them&lt;/span&gt;
    &lt;span class="c1"&gt;// logged in. The `user` argument will be the user in the database that&lt;/span&gt;
    &lt;span class="c1"&gt;// matched the username/password.&lt;/span&gt;
    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// If you want to allow this user to log in simply return the user.&lt;/span&gt;
    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// If you want to prevent someone logging in for another reason (maybe they&lt;/span&gt;
    &lt;span class="c1"&gt;// didn't validate their email yet), throw an error and it will be returned&lt;/span&gt;
    &lt;span class="c1"&gt;// by the `logIn()` function from `useAuth()` in the form of:&lt;/span&gt;
    &lt;span class="c1"&gt;// `{ message: 'Error message' }`&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="nx"&gt;user&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;usernameOrPasswordMissing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Both email and password are required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;usernameNotFound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email ${username} not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// For security reasons you may want to make this the same as the&lt;/span&gt;
      &lt;span class="c1"&gt;// usernameNotFound error so that a malicious user can't use the error&lt;/span&gt;
      &lt;span class="c1"&gt;// to narrow down if it's the username or password that's incorrect&lt;/span&gt;
      &lt;span class="na"&gt;incorrectPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Incorrect password for ${username}&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="c1"&gt;// How long a user will remain logged in, in seconds&lt;/span&gt;
    &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&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;resetPasswordOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// handler() is invoked after the password has been successfully updated in&lt;/span&gt;
    &lt;span class="c1"&gt;// the database. Returning anything truthy will automatically logs the user&lt;/span&gt;
    &lt;span class="c1"&gt;// in. Return `false` otherwise, and in the Reset Password page redirect the&lt;/span&gt;
    &lt;span class="c1"&gt;// user to the login page.&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="nx"&gt;user&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// If `false` then the new password MUST be different than the current one&lt;/span&gt;
    &lt;span class="na"&gt;allowReusedPassword&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="na"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// the resetToken is valid, but expired&lt;/span&gt;
      &lt;span class="na"&gt;resetTokenExpired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resetToken is expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// no user was found with the given resetToken&lt;/span&gt;
      &lt;span class="na"&gt;resetTokenInvalid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resetToken is invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// the resetToken was not present in the URL&lt;/span&gt;
      &lt;span class="na"&gt;resetTokenRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resetToken is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// new password is the same as the old password (apparently they did not forget it)&lt;/span&gt;
      &lt;span class="na"&gt;reusedPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must choose a new password&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signupOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Whatever you want to happen to your data on new user signup. Redwood will&lt;/span&gt;
    &lt;span class="c1"&gt;// check for duplicate usernames before calling this handler. At a minimum&lt;/span&gt;
    &lt;span class="c1"&gt;// you need to save the `username`, `hashedPassword` and `salt` to your&lt;/span&gt;
    &lt;span class="c1"&gt;// user table. `userAttributes` contains any additional object members that&lt;/span&gt;
    &lt;span class="c1"&gt;// were included in the object given to the `signUp()` function you got&lt;/span&gt;
    &lt;span class="c1"&gt;// from `useAuth()`.&lt;/span&gt;
    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// If you want the user to be immediately logged in, return the user that&lt;/span&gt;
    &lt;span class="c1"&gt;// was created.&lt;/span&gt;
    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// If this handler throws an error, it will be returned by the `signUp()`&lt;/span&gt;
    &lt;span class="c1"&gt;// function in the form of: `{ error: 'Error message' }`.&lt;/span&gt;
    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// If this returns anything else, it will be returned by the&lt;/span&gt;
    &lt;span class="c1"&gt;// `signUp()` function in the form of: `{ message: 'String here' }`.&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hashedPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userAttributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hashedPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://gravatar.com/avatar/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;?d=mp&amp;amp;s=64`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userAttributes&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="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;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// `field` will be either "username" or "password"&lt;/span&gt;
      &lt;span class="na"&gt;fieldMissing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;${field} is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;usernameTaken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email `${username}` already in use&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authHandler&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;DbAuthHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Provide prisma db client&lt;/span&gt;
    &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// The name of the property you'd call on `db` to access your user table.&lt;/span&gt;
    &lt;span class="c1"&gt;// ie. if your Prisma model is named `User` this value would be `user`, as in `db.user`&lt;/span&gt;
    &lt;span class="na"&gt;authModelAccessor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// A map of what dbAuth calls a field to what your database calls it.&lt;/span&gt;
    &lt;span class="c1"&gt;// `id` is whatever column you use to uniquely identify a user (probably&lt;/span&gt;
    &lt;span class="c1"&gt;// something like `id` or `userId` or even `email`)&lt;/span&gt;
    &lt;span class="na"&gt;authFields&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hashedPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;resetToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resetToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;resetTokenExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resetTokenExp&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;forgotPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;forgotPasswordOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loginOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resetPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resetPasswordOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signupOptions&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&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;All I did above was change the &lt;code&gt;hashedPassword&lt;/code&gt; field to &lt;code&gt;password&lt;/code&gt;, and the &lt;code&gt;username&lt;/code&gt; field to &lt;code&gt;email&lt;/code&gt;. I also replaced instances of &lt;code&gt;Username&lt;/code&gt; in messages to &lt;code&gt;Email&lt;/code&gt;, and I added the &lt;code&gt;avatarUrl&lt;/code&gt; field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding login and signup pages
&lt;/h3&gt;

&lt;p&gt;Let's add login and signup pages to the frontend. Redwood makes this really easy by providing a generator for us. Run the below command to create a login page, a signup page, and a forgot and reset password page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn rw g dbAuth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I will delete the &lt;code&gt;ForgotPassword&lt;/code&gt; and &lt;code&gt;ResetPassword&lt;/code&gt; pages, since I won't be adding that functionality to this project.&lt;/p&gt;

&lt;p&gt;Next, you need to replace the &lt;code&gt;username&lt;/code&gt; field in both Login and SignUp to &lt;code&gt;email&lt;/code&gt;, and in SignUp, add a new field called username. I've done it below and here's how your code should look like:&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="c1"&gt;// Routes.tsx&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;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Route&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;@redwoodjs/router&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;Routes&lt;/span&gt; &lt;span class="o"&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Router&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;Route&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/login"&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;LoginPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"login"&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;Route&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/signup"&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;SignupPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"signup"&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;Route&lt;/span&gt; &lt;span class="na"&gt;notfound&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;NotFoundPage&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;Router&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Routes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LoginPage.tsx&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;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;routes&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;@redwoodjs/router&lt;/span&gt;&lt;span class="dl"&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;useRef&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;react&lt;/span&gt;&lt;span class="dl"&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;Form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;PasswordField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;FieldError&lt;/span&gt;&lt;span class="p"&gt;,&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;@redwoodjs/forms&lt;/span&gt;&lt;span class="dl"&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;useAuth&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;@redwoodjs/auth&lt;/span&gt;&lt;span class="dl"&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;MetaTags&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;@redwoodjs/web&lt;/span&gt;&lt;span class="dl"&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;toast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Toaster&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;@redwoodjs/web/toast&lt;/span&gt;&lt;span class="dl"&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;useEffect&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;react&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;LoginPage&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logIn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&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="nx"&gt;isAuthenticated&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;emailRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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;emailRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&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;const&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt; &lt;span class="o"&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;data&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="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;logIn&lt;/span&gt;&lt;span class="p"&gt;({&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome back!&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="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;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MetaTags&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Login"&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="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-main"&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;Toaster&lt;/span&gt; &lt;span class="na"&gt;toastOptions&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;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rw-toast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6000&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-scaffold rw-login-container"&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-segment"&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="nt"&gt;header&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-segment-header"&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="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-heading rw-heading-secondary"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Login&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&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="nt"&gt;header&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-segment-main"&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-form-wrapper"&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;Form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-form-wrapper"&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;Label&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label rw-label-error"&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    Email
                  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Label&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;TextField&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input rw-input-error"&lt;/span&gt;
                    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                    &lt;span class="na"&gt;validation&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;required&lt;/span&gt;&lt;span class="p"&gt;:&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="kc"&gt;true&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is required&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="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;FieldError&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-field-error"&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;Label&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label rw-label-error"&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    Password
                  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Label&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;PasswordField&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input rw-input-error"&lt;/span&gt;
                    &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"current-password"&lt;/span&gt;
                    &lt;span class="na"&gt;validation&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;required&lt;/span&gt;&lt;span class="p"&gt;:&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="kc"&gt;true&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password is required&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="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;FieldError&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-field-error"&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-button-group"&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;Submit&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-button rw-button-blue"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Login&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Submit&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="nt"&gt;div&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;Form&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-login-link"&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="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Don&lt;span class="ni"&gt;&amp;amp;apos;&lt;/span&gt;t have an account?&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-link"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              Sign up!
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;LoginPage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;routes&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;@redwoodjs/router&lt;/span&gt;&lt;span class="dl"&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;useRef&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;react&lt;/span&gt;&lt;span class="dl"&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;Form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;PasswordField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;FieldError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Submit&lt;/span&gt;&lt;span class="p"&gt;,&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;@redwoodjs/forms&lt;/span&gt;&lt;span class="dl"&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;useAuth&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;@redwoodjs/auth&lt;/span&gt;&lt;span class="dl"&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;MetaTags&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;@redwoodjs/web&lt;/span&gt;&lt;span class="dl"&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;toast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Toaster&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;@redwoodjs/web/toast&lt;/span&gt;&lt;span class="dl"&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;useEffect&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;react&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;SignupPage&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signUp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&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="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="c1"&gt;// focus on email box on page load&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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;emailRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&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;const&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt; &lt;span class="o"&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;data&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="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;signUp&lt;/span&gt;&lt;span class="p"&gt;({&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// user is signed in automatically&lt;/span&gt;
      &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome!&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="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;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MetaTags&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Signup"&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="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-main"&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;Toaster&lt;/span&gt; &lt;span class="na"&gt;toastOptions&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;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rw-toast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6000&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-scaffold rw-login-container"&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-segment"&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="nt"&gt;header&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-segment-header"&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="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-heading rw-heading-secondary"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Signup&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&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="nt"&gt;header&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-segment-main"&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-form-wrapper"&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;Form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-form-wrapper"&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;Label&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label rw-label-error"&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    Email
                  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Label&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;TextField&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input rw-input-error"&lt;/span&gt;
                    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                    &lt;span class="na"&gt;validation&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;required&lt;/span&gt;&lt;span class="p"&gt;:&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="kc"&gt;true&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is required&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="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;FieldError&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-field-error"&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;Label&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label rw-label-error"&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    Username
                  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Label&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;TextField&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input rw-input-error"&lt;/span&gt;
                    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                    &lt;span class="na"&gt;validation&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;required&lt;/span&gt;&lt;span class="p"&gt;:&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="kc"&gt;true&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Username is required&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="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;FieldError&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-field-error"&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;Label&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-label rw-label-error"&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    Password
                  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Label&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;PasswordField&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input"&lt;/span&gt;
                    &lt;span class="na"&gt;errorClassName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-input rw-input-error"&lt;/span&gt;
                    &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"current-password"&lt;/span&gt;
                    &lt;span class="na"&gt;validation&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;required&lt;/span&gt;&lt;span class="p"&gt;:&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="kc"&gt;true&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password is required&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="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;FieldError&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-field-error"&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-button-group"&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;Submit&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-button rw-button-blue"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                      Sign Up
                    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Submit&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="nt"&gt;div&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;Form&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-login-link"&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="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Already have an account?&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rw-link"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              Log in!
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;SignupPage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Notice how I've only changed the text but not the &lt;code&gt;name&lt;/code&gt; of the inputs? This is because under-the-hood, Redwood still expects your &lt;code&gt;email&lt;/code&gt; to be called &lt;code&gt;username&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For one final step, let's add a home page so we don't have to see the Redwood logo anymore. Use the below command to generate an index page at &lt;code&gt;/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn rw g page home /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will generate a page called &lt;code&gt;home&lt;/code&gt;, but map it to &lt;code&gt;/&lt;/code&gt;, instead of &lt;code&gt;/home&lt;/code&gt;. Change the code of the newly created &lt;code&gt;HomePage.tsx&lt;/code&gt; to this:&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;MetaTags&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;@redwoodjs/web&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;HomePage&lt;/span&gt; &lt;span class="o"&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="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MetaTags&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Redwoodit"&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"A clone of Reddit using RedwoodJS"&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="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Redwoodit&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;HomePage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, you've added authentication to your Redwood application.&lt;/p&gt;

&lt;p&gt;If you visit &lt;a href="http://localhost:3000/signup" rel="noopener noreferrer"&gt;http://localhost:3000/signup&lt;/a&gt;, you can create an account and if you visit &lt;a href="http://localhost:3000/login" rel="noopener noreferrer"&gt;http://localhost:3000/login&lt;/a&gt;, you can log in to an account.&lt;/p&gt;

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

&lt;p&gt;You've successfully created a Redwood application and added authentication to it. In the next part of this tutorial, you will add support for fetching, creating, updating and deleting posts and comments. If you've gotten stuck anywhere, be sure to check out the &lt;a href="https://gitlab.com/arnu515-tutorials/reddit-clone-with-redwoodjs" rel="noopener noreferrer"&gt;source code&lt;/a&gt;, the &lt;a href="https://redwoodjs.com/docs" rel="noopener noreferrer"&gt;Redwood documentation&lt;/a&gt;, or ask in the &lt;a href="https://discord.gg/jjSYEQd" rel="noopener noreferrer"&gt;Redwood Discord&lt;/a&gt;/&lt;a href="https://community.redwoodjs.com/" rel="noopener noreferrer"&gt;Discourse Forums&lt;/a&gt; for help.&lt;/p&gt;

&lt;p&gt;Stay tuned for Part 2!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Simple Authenticator - The Simplest TOTP Application</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Fri, 14 Jan 2022 07:30:51 +0000</pubDate>
      <link>https://dev.to/arnu515/simple-authenticator-the-simplest-totp-application-94f</link>
      <guid>https://dev.to/arnu515/simple-authenticator-the-simplest-totp-application-94f</guid>
      <description>&lt;h3&gt;
  
  
  Overview of My Submission
&lt;/h3&gt;

&lt;p&gt;Simple Authenticator is an application that generates TOTP codes for two-factor authentication - like Google Authenticator. It can optionally store your configured applications &lt;strong&gt;encrypted&lt;/strong&gt; in the cloud with MongoDB Atlas.&lt;/p&gt;

&lt;p&gt;Github Repository: &lt;a href="https://github.com/arnu515/simple-authenticator" rel="noopener noreferrer"&gt;https://github.com/arnu515/simple-authenticator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Link to download (Android only): &lt;a href="https://github.com/arnu515/simpleauthenticator/releases" rel="noopener noreferrer"&gt;https://github.com/arnu515/simpleauthenticator/releases&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Submission Category:
&lt;/h3&gt;

&lt;p&gt;[Note]: Choose your own Adventure&lt;/p&gt;

&lt;h3&gt;
  
  
  Link to Code
&lt;/h3&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&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%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/arnu515" rel="noopener noreferrer"&gt;
        arnu515
      &lt;/a&gt; / &lt;a href="https://github.com/arnu515/simpleauthenticator" rel="noopener noreferrer"&gt;
        simpleauthenticator
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Simple TOTP Authenticator App built with Flutter
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;simpleauthenticator&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Simple TOTP Authenticator&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Download&lt;/h2&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I recommend running the application with &lt;code&gt;flutter run&lt;/code&gt;, because the APK is not signed&lt;/strong&gt;. View instructions below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Get the Android APK file from &lt;a href="https://github.com/arnu515/simpleauthenticator/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You may get a play-protect warning, and that's because I haven't yet signed the application. This warning can be ignored.&lt;/p&gt;
&lt;p&gt;I will publish it soon on F-Droid and Play store.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run locally&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;You can run the application on Windows, Mac or Linux by using the below command. Make sure to have the &lt;a href="https://flutter.dev" rel="nofollow noopener noreferrer"&gt;flutter SDK&lt;/a&gt; installed.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;flutter run --dart-define &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;API_URL=URL_TO_BACKEND&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;If you haven't hosted the &lt;a href="https://github.com/arnu515/simpleauthenticator/tree/master/backend" rel="noopener noreferrer"&gt;backend&lt;/a&gt; yourself, you can use &lt;a href="https://d13c320db282.up.railway.app" rel="nofollow noopener noreferrer"&gt;https://d13c320db282.up.railway.app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can also run the app on Android if you have the Android SDK installed by connecting your phone to your computer and turning on USB debugging, and running the above command.&lt;/p&gt;
&lt;p&gt;You can also build the app by replacing &lt;code&gt;flutter run&lt;/code&gt; with &lt;code&gt;flutter build&lt;/code&gt;…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/arnu515/simpleauthenticator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>atlashackathon</category>
      <category>mongodb</category>
      <category>flutter</category>
      <category>dart</category>
    </item>
    <item>
      <title>Connect VSCode to a Docker container</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Sat, 18 Sep 2021 02:40:24 +0000</pubDate>
      <link>https://dev.to/arnu515/connect-vscode-to-a-docker-container-50o7</link>
      <guid>https://dev.to/arnu515/connect-vscode-to-a-docker-container-50o7</guid>
      <description>&lt;p&gt;In the video below, you'll learn how you can connect VSCode to a Docker container using the Remote Containers extension.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/2Fx_4W7sO8Y"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>devops</category>
      <category>vscode</category>
      <category>docker</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Automate your workflow with Microsoft Power Automate</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Mon, 12 Jul 2021 16:50:41 +0000</pubDate>
      <link>https://dev.to/arnu515/automate-your-workflow-with-microsoft-power-automate-3m7i</link>
      <guid>https://dev.to/arnu515/automate-your-workflow-with-microsoft-power-automate-3m7i</guid>
      <description>&lt;p&gt;The most tedious part of development is doing the same tasks over and over again. Don't you feel bored having to mention someone on Slack when they get assigned a Github Issue, or having to send an email to your newsletter when you post a tweet.&lt;/p&gt;

&lt;p&gt;Sure, you can setup integrations, webhooks and CI jobs to do these tasks, but they're often tedious, require signups to hundreds of services, and all of them might not have the same steps.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://flow.microsoft.com" rel="noopener noreferrer"&gt;Microsoft Power Automate&lt;/a&gt;, also called Microsoft Flow, which allows you to automate almost everything in your tech stack. Want to send a message on Slack or Teams, sure, want to SMS a number with Twilio, you can do that too. It also has many listeners, like when a new Tweet is posted, or when a Github Issue is assigned to you.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll take a look at how we can automate a few common tasks with Microsoft Flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Office365 Microsoft Account&lt;/li&gt;
&lt;li&gt;Accounts for the services we'll be automating (Github, Slack, etc).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Flow 1 - Mail me when a Github Issue is assigned to me
&lt;/h2&gt;

&lt;p&gt;First, create a new Automated Cloud flow and give it a name.&lt;/p&gt;

&lt;p&gt;Add the &lt;code&gt;When an issue is assigned to me&lt;/code&gt; trigger from Github. You will now be asked to sign in to Github, which you should do.&lt;/p&gt;

&lt;p&gt;Next, add the &lt;code&gt;Mail&lt;/code&gt; action, which uses Sendgrid under the hood.&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%2Fi.ibb.co%2FXbgBN01%2FScreenshot-from-2021-07-09-14-48-42.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%2Fi.ibb.co%2FXbgBN01%2FScreenshot-from-2021-07-09-14-48-42.png" alt="How your flow looks"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can notice that I've added some dynamic elements that were provided to me by the Github Trigger.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can add more things to your flow, for example, add an item on Microsoft Todo, create a Trello card, or message yourself on Slack.&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%2Fi.ibb.co%2Fg4DGNVs%2FScreenshot-from-2021-07-09-14-53-15.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%2Fi.ibb.co%2Fg4DGNVs%2FScreenshot-from-2021-07-09-14-53-15.png" alt="Added a Todo action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;code&gt;Save&lt;/code&gt; when done, and you will now be emailed whenever an issue's been assigned to you!&lt;/p&gt;

&lt;h2&gt;
  
  
  Flow 2 - Post your website's status to your Twitter everyday at 6 AM
&lt;/h2&gt;

&lt;p&gt;Create a Scheduled Cloud Flow, since this event is based on time.&lt;/p&gt;

&lt;p&gt;Make sure the date is set to this date, and the time is set to &lt;code&gt;6:00 AM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also set the task to repeat every 1 day. See the below image if you get confused.&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%2Fi.ibb.co%2F0Bf8qqL%2FScreenshot-from-2021-07-09-14-58-28.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%2Fi.ibb.co%2F0Bf8qqL%2FScreenshot-from-2021-07-09-14-58-28.png" alt="Flow creation dialog"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add a HTTP action that'll query your website's status page. My website uses Cachet to display its status, so I'll tailor my flow to Cachet's API response.&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%2Fi.imgur.com%2FLbm4ZML.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%2Fi.imgur.com%2FLbm4ZML.png" alt="HTTP action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, you need to parse the data down to access the &lt;code&gt;status_name&lt;/code&gt; field in the JSON response. &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%2Fi.imgur.com%2FPwNrtPK.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%2Fi.imgur.com%2FPwNrtPK.png" alt="Data actions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, you can tweet out the status of the website. You'll need to sign in to Twitter for this flow to work.&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%2Fi.imgur.com%2FHujqhpF.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%2Fi.imgur.com%2FHujqhpF.png" alt="Tweet action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;code&gt;Save&lt;/code&gt; when done, and now, at 6 AM daily, your followers will be notified of your website's status.&lt;/p&gt;

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

&lt;p&gt;See how easy it is to automate the small things? The best part is that Microsoft Flow is included with the Office365 subscription, so along with Excel, Word and PowerPoint, you also get this monster of an automation tool that really, nobody cares about! So go ahead and share this article with your friends, so they can also use this wonderful tool.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>azure</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Everything new in Flask 2.0</title>
      <dc:creator>arnu515</dc:creator>
      <pubDate>Tue, 22 Jun 2021 15:49:25 +0000</pubDate>
      <link>https://dev.to/arnu515/everything-new-in-flask-2-0-gnl</link>
      <guid>https://dev.to/arnu515/everything-new-in-flask-2-0-gnl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@markusspiske" rel="noopener noreferrer"&gt;Markus Spiske&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/code" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Flask is one of the most used Python Frameworks in Web Development. It recently came out with a new release - &lt;code&gt;2.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In this post, we'll go through the major changes. If you want the full change log, click &lt;a href="https://flask.palletsprojects.com/en/2.0.x/changes/#version-2-0-0" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changes
&lt;/h2&gt;

&lt;p&gt;Let's go over the major changes introduced in Flask &lt;code&gt;2.0.0&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dropped support for Python 2 and 3.5
&lt;/h3&gt;

&lt;p&gt;The most breaking change is the dropped support for Python &lt;code&gt;2&lt;/code&gt; and &lt;code&gt;3.5&lt;/code&gt;. Well, it's about time we move from those versions anyway, since Python &lt;code&gt;2&lt;/code&gt; is &lt;em&gt;really old&lt;/em&gt;!&lt;/p&gt;

&lt;p&gt;Also, dropping &lt;code&gt;3.5&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; support is required for the next change:&lt;/p&gt;

&lt;h3&gt;
  
  
  Type hints
&lt;/h3&gt;

&lt;p&gt;Python &lt;code&gt;3.7&lt;/code&gt; added support for type annotations. This is where you can add type hints to variables to tell others what type it will be. This is a God send for people with linters and autocompletors, since you no longer have to view docs to find the return type of a certain function.&lt;/p&gt;

&lt;p&gt;Keep in mind however, that type hints aren't enforced, meaning that this is completely valid code, as far as the &lt;code&gt;python&lt;/code&gt; interpreter goes&lt;/p&gt;

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

&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;


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

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;To check your types, use an external linter like &lt;a href="http://mypy-lang.org/" rel="noopener noreferrer"&gt;mypy&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Flask &lt;code&gt;2.0.0&lt;/code&gt; is now fully typed, so no more awkward moments when you import &lt;code&gt;request&lt;/code&gt; from &lt;code&gt;flask&lt;/code&gt; and your IDE won't autocomplete its methods.&lt;/p&gt;

&lt;p&gt;You can see the difference between the type hints in Flask &lt;code&gt;2.0.0&lt;/code&gt; and its earlier versions with the &lt;code&gt;help()&lt;/code&gt; function:&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%2Fi.imgur.com%2Fsoc5ohb.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%2Fi.imgur.com%2Fsoc5ohb.png" alt="Flask 1.1.4's type hints"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above image shows Flask &lt;code&gt;1.1.4&lt;/code&gt;'s docstring and the below image shows Flask &lt;code&gt;2.0.1&lt;/code&gt;'s docstring. Notice the type hints&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%2Fi.imgur.com%2FpK5B9yK.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%2Fi.imgur.com%2FpK5B9yK.png" alt="Flask 2.0.1's type hints"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  New &lt;code&gt;Config.from_file()&lt;/code&gt; method
&lt;/h3&gt;

&lt;p&gt;You may remember the &lt;code&gt;Config.from_json()&lt;/code&gt;, or the &lt;code&gt;app.config.from_json()&lt;/code&gt; method that you use to configure Flask with a JSON File. In Flask &lt;code&gt;2.0.0&lt;/code&gt;, it has been deprecated in favour of &lt;code&gt;Config.from_file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The syntax of this method is as follows:&lt;/p&gt;

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

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loads_function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Now, if we wanted to implement the old &lt;code&gt;from_json&lt;/code&gt; behaviour, look at the code below:&lt;/p&gt;

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

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This opens us to do the same to parse &lt;code&gt;TOML&lt;/code&gt; files as well:&lt;/p&gt;

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

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;toml&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;You need to install the &lt;code&gt;toml&lt;/code&gt; package for this!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  New route decorators
&lt;/h3&gt;

&lt;p&gt;Flask has new decorators for defining routes now. Before we used &lt;code&gt;@app.route(path: str)&lt;/code&gt; to define a route in our app, and for methods other than &lt;code&gt;GET&lt;/code&gt; we add the &lt;code&gt;methods&lt;/code&gt; parameter to our decorator.&lt;/p&gt;

&lt;p&gt;Now, Flask has followed ExpressJS's routes and added decorators for defining routes specific to HTTP Methods.&lt;/p&gt;

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

&lt;span class="c1"&gt;# You can now use these instead of app.route
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@app.put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@app.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@app.patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Smaller changes
&lt;/h3&gt;

&lt;p&gt;Here's a list of smaller changes that may affect your projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some &lt;code&gt;send_file&lt;/code&gt; parameters have been renamed, the old names are deprecated. &lt;code&gt;attachment_filename&lt;/code&gt; is renamed to &lt;code&gt;download_name&lt;/code&gt;. &lt;code&gt;cache_timeout&lt;/code&gt; is renamed to &lt;code&gt;max_age&lt;/code&gt;. &lt;code&gt;add_etags&lt;/code&gt; is renamed to &lt;code&gt;etag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When loading a &lt;code&gt;.env&lt;/code&gt; or &lt;code&gt;.flaskenv&lt;/code&gt; file, the current working directory is no longer changed to the location of the file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;helpers.total_seconds()&lt;/code&gt; is deprecated. Use &lt;code&gt;timedelta.total_seconds()&lt;/code&gt; instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And my favorite feature of this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;flask shell&lt;/code&gt; sets up tab and history completion like the default python shell if &lt;code&gt;readline&lt;/code&gt; is installed&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to upgrade
&lt;/h2&gt;

&lt;p&gt;Change your &lt;code&gt;Flask&lt;/code&gt; dependency in &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;Pipfile&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt; to this:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

Flask==2.0.0


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

&lt;/div&gt;

&lt;p&gt;And run:&lt;/p&gt;

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

pip3 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt


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

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;You can no longer use &lt;code&gt;python2&lt;/code&gt;, so beware of that!&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;This is of course not the full change log. If you want the full change log, click &lt;a href="https://flask.palletsprojects.com/en/2.0.x/changes/#version-2-0-0" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>webdev</category>
      <category>changelog</category>
    </item>
  </channel>
</rss>
