<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ansell Maximilian</title>
    <description>The latest articles on Forem by Ansell Maximilian (@ansellmaximilian).</description>
    <link>https://forem.com/ansellmaximilian</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%2F561391%2Fe5826c5f-14a9-4405-a160-14201379dddf.png</url>
      <title>Forem: Ansell Maximilian</title>
      <link>https://forem.com/ansellmaximilian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ansellmaximilian"/>
    <language>en</language>
    <item>
      <title>Pipfolio: Pip-Boy Inspired Web Portfolio</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Fri, 30 Jan 2026 09:10:05 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/pipfolio-pip-boy-inspired-web-portfolio-4hpm</link>
      <guid>https://forem.com/ansellmaximilian/pipfolio-pip-boy-inspired-web-portfolio-4hpm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/new-year-new-you-google-ai-2025-12-31"&gt;New Year, New You Portfolio Challenge Presented by Google AI&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About Me
&lt;/h2&gt;

&lt;p&gt;First and foremost, I'm a big Fallout fan, specifically Fallout: New Vegas. As proven by my playtime on Steam:&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%2Fw9cfv3h7exqw2sb2neq0.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%2Fw9cfv3h7exqw2sb2neq0.png" alt="FNV Playtime"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the current ongoing season of the Fallout adaptation TV show centered around New Vegas, I could not think of a better theme for my new portfolio to show off my skills as well as my interest in a single glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portfolio
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://pip-boy-portfolio-978859030334.us-west1.run.app"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;




&lt;p&gt;Here's the live website: &lt;a href="https://pip-boy-portfolio-978859030334.us-west1.run.app" rel="noopener noreferrer"&gt;https://pip-boy-portfolio-978859030334.us-west1.run.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the repo if you're interested: &lt;a href="https://github.com/AnsellMaximilian/pip-boy-portfolio" rel="noopener noreferrer"&gt;https://github.com/AnsellMaximilian/pip-boy-portfolio&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I built this entirely using Google's AI Studio. I must say it went very well. I'll give you the exact first prompt I gave it word for word. No planning; just natural language:&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%2Fjmfibdlach69is4174mg.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%2Fjmfibdlach69is4174mg.png" alt="First prompt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, I kept it simple at first just so I could see the design it would cook up and, if needed, adjust if I weren't satisfied.&lt;/p&gt;

&lt;p&gt;Crucially, I gave it sample images of the actual Pip-Boy to give it some inspiration:&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%2Fa31bukalzvbdjfllhwof.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%2Fa31bukalzvbdjfllhwof.png" alt="Example reference"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The current app looks a lot more advanced that what this first prompt created, but thanks to AI Studio's version history feature, I can show you exactly what it built the first time around:&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%2Fb5hz6dp1piaw0plzz8sr.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%2Fb5hz6dp1piaw0plzz8sr.png" alt="First prompt result"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see; it's already pretty good. But it was far from finished. So  after a few more prompts and adjustment... I ended with something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30j4wnmpen27ifznmdt5.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%2F30j4wnmpen27ifznmdt5.png" alt="Final page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To look at the full thing, make sure to visit the deployed version above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Most Proud Of
&lt;/h2&gt;

&lt;p&gt;AI Studio really got it perfect at the start. The theme, colors, and the terminal effects all really sell the vibes well.&lt;/p&gt;

&lt;p&gt;I tried to get it to maintain that design to the very end, and it worked great!&lt;/p&gt;

&lt;p&gt;I really like the contacts section disguised as the "Radio" menu in the Pip-Boy:&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%2Fboe98fgkv96iqdi7dmlp.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%2Fboe98fgkv96iqdi7dmlp.png" alt="Radio Menu"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Something cool that happened during development: I basically gave a link to my old portfolio website and asked it to parse it and put my skills there into the S.P.E.C.I.A.L section in the Pipfolio, and it did exactly that.&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%2F3t61bnszj5xpatr5627e.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%2F3t61bnszj5xpatr5627e.png" alt="Special Menu"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>portfolio</category>
      <category>gemini</category>
    </item>
    <item>
      <title>From a Single File to a Full Chrome Extension Using Kiro</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Tue, 02 Dec 2025 01:09:06 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/from-a-single-file-to-a-full-chrome-extension-using-kiro-4hld</link>
      <guid>https://forem.com/ansellmaximilian/from-a-single-file-to-a-full-chrome-extension-using-kiro-4hld</guid>
      <description>&lt;h2&gt;
  
  
  Quick Demo
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/s_0lJVnz8Ys"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There have been an increasing variety of coding agents recently. Each one getting more integrated and comprehensive than the last. I've been lucky enough to have been able to try a couple of these not only on my own free time, but as part of an implementation task at my work to integrate these agents into some of the development workflows.&lt;/p&gt;

&lt;p&gt;I remember trying CrewAI, a multi-agent AI system, and trying to hack together our own coding agent. It did well enough... but it's not what CrewAI was meant for.&lt;/p&gt;

&lt;p&gt;Then Codex came along and did what our hacky system did way better and more precisely with fewer errors. The learning experience was valuable nonetheless. Because that's what it was meant for. It reads your codebase and prepares itself with the right context.&lt;/p&gt;

&lt;p&gt;Then I saw over on Devpost that there was a &lt;a href="https://kiroween.devpost.com/" rel="noopener noreferrer"&gt;hackathon&lt;/a&gt;for one of the newest coding agents around the block: &lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is Kiro?
&lt;/h3&gt;

&lt;p&gt;Plainly, Kiro is an AI-powered Integrated Development Environment. So unlike codex it comes with a full coding environment and extra purpose-specific features on top of it. Those features include but are not limited to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating agent hooks: Agent automation that runs after a specific trigger that you set. For example, you can literally ask Kiro to create an agent hook that updates test files every time the relevant files change; Kiro will automatically create this hook for you.&lt;/li&gt;
&lt;li&gt;Steering docs: You can ask Kiro to generate steering documents for your project. This will give Kiro AND yourself a better understanding of the whole structure of your project. I find this is especially useful for older projects you've inherited from other devs. This way you don't have to waste much time studying legacy code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Actually, why even read the docs yourself. Just ask Kiro about the codebase. Now thanks to the steering docs, it can answer your questions better and more accurately. E.g. "How are the real time messages handled within this application? Where did they place the socket listeners?". You no longer have to trace it down from the very beginning of the app flow.&lt;/p&gt;

&lt;h4&gt;
  
  
  Spec-Driven Development
&lt;/h4&gt;

&lt;p&gt;Here's what I think sets apart Kiro from other coding agents: Spec-Driven Development. A common denominator between most of the coding agents is how you usually approach development: vibe coding.&lt;/p&gt;

&lt;p&gt;You say what you want, and the agent gives you the code: &lt;code&gt;prompt&lt;/code&gt; -&amp;gt; &lt;code&gt;code&lt;/code&gt;. But Kiro, while still providing the option to vibe code, provides a more thorough alternative. This is spec driven development:&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%2Fzyfsccr9xztnyhw0dkwr.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%2Fzyfsccr9xztnyhw0dkwr.png" alt="Choosing spec mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prompting in this mode won't get you code straight away; instead, Kiro will think through your request(s), plan accordingly, create an appropriate spec folder, and fill it with relevant specification documents, namely: &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%2Fo9wwddgym2wbo4tk1sez.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%2Fo9wwddgym2wbo4tk1sez.png" alt="Spec documents"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will be generated for you to review before you move on to implementing the code. That's what I really like about this. That buffer stage you can utilize to check if what the agent is planning to do matches with what you want.&lt;/p&gt;

&lt;p&gt;Beside basic implementation, it will also require and list thorough testing that it will execute for you. So once you're satisfied with the spec, you can go ahead and start the tasks in the &lt;code&gt;tasks.md&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kiro will also fix any errors it finds during testing&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;I had noticed that I had been losing my ability to focus lately. Reduced attention spans and easily distracted -- basically that brainrot thing people keep mentioning online. It had been hard to pry my attention off just doom scrolling and opening X/Twitter. It was literally impossible for me to do a focus session without getting distracted.&lt;/p&gt;

&lt;p&gt;Then I saw this really good video on dopamine detox. The essence is basically this: My brain has been getting too much frequent high-dopamine, fast reward activities -- like scrolling Reddit or watching Shorts. My brain got used to these and basically dampened my receptors towards sustained, low-intensity reward over time. That's why I couldn't stay focused on my work for a long time.&lt;/p&gt;

&lt;p&gt;It also said that my brain would benefit more from a more rigid system of &lt;code&gt;work THEN reward&lt;/code&gt;. It also said it would benefit from gamification. That's the core fusion/chimera idea this project.&lt;/p&gt;

&lt;h4&gt;
  
  
  An Chrome Extension that Combines Productivity with Fun
&lt;/h4&gt;

&lt;p&gt;I then came up with "Soulful Sessions". A Halloween themed Chrome Extension that combines those two core ideas: &lt;code&gt;work THEN reward&lt;/code&gt; AND gamification.&lt;/p&gt;

&lt;p&gt;I also came up with a unique concept/theme surrounding the core functionality of the extension. To stick with the prompt's Halloween theme: I came up with the center idea of "soul collecting".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is one of the most underrated benefits of Kiro. Allowing me to put more attention to the creative side of this project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You play as the "Soul Shepherd", a benevolent grim reaper-like character who collects souls and lead them into a better place. You have stats like Spirit (attack), Soulflow (how fast you idly generate resources), and Harmony (basically critical chance).&lt;/p&gt;

&lt;p&gt;There's two types of gameplay here: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;idle&lt;/code&gt;: where you generate "Soul Embers" (main currency) passively. The idea is the Soul Shepherd collects "content souls". Souls that are ready to move on. So there's no effort required. You just generate Soul Embers based on your current stats.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bosses&lt;/code&gt;: where you battle a "Stubborn Soul", souls that are not ready to move on yet, even though it's their time. These are like campaigns or bosses on games. Each boss has a unique name, backstory, and "Soul Resolve" (health points).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You defeat these Stubborn Souls by doing "Focus Sessions". A period you start during which you cannot visit discouraged or blocked sites you've set in the settings. After each focus session, you deal damage to the boss according to your stats.&lt;/p&gt;

&lt;p&gt;For example: let's say I've set &lt;code&gt;facebook.com&lt;/code&gt; as one of the discouraged sites, and &lt;code&gt;youtube.com&lt;/code&gt; as one of the blocked sites.&lt;/p&gt;

&lt;p&gt;During a focus session, if I tried to visit Facebook, I'll get a light warning saying my session has been compromised and that my rewards have been reduced. But I'll still be able to click around in it, since I only have it as discouraged.&lt;/p&gt;

&lt;p&gt;If I visited YouTube, since it's in the blocked list, I'll actually be immediately redirected to a special page saying I can't visit it. So no chance for me to actually get distracted by YouTube.&lt;/p&gt;

&lt;p&gt;Then, once the focus session is over, depending on a couple of factors including whether or not my session was compromised after visiting a discouraged or blocked site, I'll get my rewards, which are the Soul Embers we talked about and "Soul Insight", the XP with which you level up.&lt;/p&gt;

&lt;p&gt;Then we enter "Break Mode". This is the &lt;code&gt;reward&lt;/code&gt; part of &lt;code&gt;work THEN reward&lt;/code&gt;. We'll be able to do lots of things during this period, including using the Soul Embers we generated to upgrade skills and buy cosmetics: "Themes" and "Sprites" (your Soul Shepherd's unlockable skins).&lt;/p&gt;

&lt;p&gt;If you leveled up, you also get skill points to use on upgrading your skills. Since upgrading skills using Soul Embers gets exponentially more expensive as you do it.&lt;/p&gt;

&lt;p&gt;That's the core of the app, but I'll show you screenshots of the whole extension a little further below. For now let's talk about developing using Kiro:&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with One Manifest File
&lt;/h2&gt;

&lt;p&gt;Alright, I've rambled on long enough. Let's get to coding-- or rather let's let Kiro get to coding.&lt;/p&gt;

&lt;p&gt;I actually made it a point to let Kiro do most of the coding and debugging. A sort of "stress test" for this new tech I've discovered. To that end, I gave Kiro almost next to nothing to start with: A Chrome extension manifest JSON file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Looking back, I probably could've given Kiro an empty folder and it would've still worked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I turned on Kiro's spec mode and gave it a MASSIVE and detailed description of my whole app. I basically listed the entire extension idea I had after a couple hours of brainstorming.&lt;/p&gt;

&lt;p&gt;Here's a glimpse of my first prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
Core Fantasy &amp;amp; Tone

You are a **gentle Grim Reaper** type – more guide than killer.

* You help **Content Souls** (peaceful, ready-to-move-on spirits) pass on.
* You work to calm **Stubborn Souls** (bosses) who cling to unfinished business.
* Everything is **spiritual, emotional, non-violent**: persuasion, soothing, guiding, not attacking.

Think of Death as a mix of therapist, guide, and cosmic janitor.

---
TRUNCATED

...AROUND 400 MORE LINES OF IDEAS AND FEATURE REQUESTS...

---

Daily User Experience (End-to-End)

1. User installs extension.
2. Opens Options Page:

   * sets focus length (e.g. 25 min)
   * lists distraction sites
   * sets goals/tasks/subtasks
3. Clicks extension → Popup:

   * chooses task to work on
   * starts focus session
4. During session:

   * popup locked with simple message
   * background tracks time &amp;amp; distractions
5. Session ends:

   * notification: “Your ritual is complete.”
   * popup shows rewards (Soul Insight, Soul Embers, boss progress)
6. Break:

   * user checks game, upgrades stats, watches idle souls being collected
7. After break:

   * nudged to start another session
   * rinse, repeat: this is their new Work → Reward / detox loop.

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

&lt;/div&gt;



&lt;p&gt;Again, I really wanted to stress test my new friend, Kiro, here. It generated a giant spec for me and listed a whopping &lt;strong&gt;42&lt;/strong&gt; tasks in the &lt;code&gt;tasks.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I looked through the documentations and then when satisfied, I ran all 42 tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sidenote: Creating an Agent Hook&lt;/strong&gt;&lt;br&gt;
Quick detour! I wanted to mention that I created a automatic hook to help organize my commits as Kiro performs the tasks it gave itself.&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%2Fsumuxhyshu5a5k9v0rwj.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%2Fsumuxhyshu5a5k9v0rwj.png" alt="Creating a hook"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And yes, you can ask Kiro to create a hook for you. This worked well enough, except that agent hooks and tasks all fall under one category: a task. So when you simultaneously queue all your tasks, this agent hook will only be queued at the bottom (after all the tasks) and therefore you'll only activate this hook for the last task.&lt;/p&gt;

&lt;p&gt;I'm sure there's some hack you can do though, maybe instead of queuing all the tasks, you could let Kiro do that. Maybe create a last commit file, let the previous agent hook to also update that commit file and create another hook triggered by changes in that commit file to automatically start the next task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detour over!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Okay, where were we? Kiro ran all 42 tasks. It generated all the code and respective tests it outlined in the requirements. It executed all the tests. Here are some important observations I made:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kiro will prepare your project and install all the necessary packages for testing, if hasn't already.&lt;/li&gt;
&lt;li&gt;Kiro will proactively fix all errors generated by the tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Okay, so it did its 42 tasks. How'd it do? It IS a big requirement list!&lt;/p&gt;

&lt;p&gt;Can I load the extension?&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging with Kiro
&lt;/h2&gt;

&lt;p&gt;Actually, I ran into some loading errors (can't remember what -- sorry!) from the first version generated by Kiro from that task list.&lt;/p&gt;

&lt;p&gt;No surprise there though. The scope was so large it was practically guaranteed. Let's see if we can let Kiro fix it itself. Enter vibe coding with Kiro. That's right, I haven't forgotten about it.&lt;/p&gt;

&lt;p&gt;So I simply copied the error Chrome gave me and gave it to Kiro. I don't even remember what it did, but it searched through my codebase again, thinking and talking to himself, and then... it's fixed! I can load the extension without issue.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pretty impressive!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Initial MVP
&lt;/h2&gt;

&lt;p&gt;Before I show you the final product, I'd like to show screenshots of the first working iteration Kiro generated for me. Just to show you how it did, and how it compares to the final product:&lt;/p&gt;

&lt;h3&gt;
  
  
  Main Popup UI
&lt;/h3&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%2Fnxn4oft55d4z8zmvoiup.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%2Fnxn4oft55d4z8zmvoiup.png" alt="MVP popup UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the main UI where you start your focus sessions. It also lists all the important details about your current status. Skills, levels, Soul Embers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Popup During Focus Session
&lt;/h3&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%2Fwskzfvkabc51pz0e1xfg.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%2Fwskzfvkabc51pz0e1xfg.PNG" alt="MVP popup UI during focus session"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just like I asked it to, it disabled all game UI while focus session is active, so you can't even get distracted by the app itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discouraged and Blocked Sites
&lt;/h3&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%2Fhyhr3v0jmjq7ia12m59y.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%2Fhyhr3v0jmjq7ia12m59y.png" alt="MVP Visiting a discouraged site"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is what happens when I visit a site I listed as "discouraged". It does exactly as requested, giving a small warning to persuade players to get back to focusing.&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%2F5z34i80fafzj4lelamui.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%2F5z34i80fafzj4lelamui.png" alt="MVP visiting a blocked site"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is what happens when I enable strict mode and visit a site listed as "blocked". Blocks are stronger than discouragement because it literally prevents me from visiting blocked sites by redirecting me to this special page. I call it "hard blocks" instead of the previous "soft blocks".&lt;/p&gt;

&lt;h3&gt;
  
  
  Break Time
&lt;/h3&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%2F7m5eu4ee2ajzcl9jq8g6.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%2F7m5eu4ee2ajzcl9jq8g6.PNG" alt="Notification"&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%2Fpssyfcswxhy9xis8l849.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%2Fpssyfcswxhy9xis8l849.png" alt="MVP Break Time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once focus session ends, you'll get a notification and the popup now goes into "Break Time" mode. This is the &lt;code&gt;reward&lt;/code&gt; part of &lt;code&gt;work THEN reward&lt;/code&gt;. During this limited reward time, you'll get to upgrade your Soul Shepherd using Soul Embers or skill points you gain from leveling up.&lt;/p&gt;

&lt;p&gt;If you have enough, you'd also be able to buy cosmetics during this time.&lt;/p&gt;

&lt;p&gt;And as you can see, you will have done some spirit damage to the Stubborn Soul, resulting in its Soul Resolve (health) dropping some points.&lt;/p&gt;

&lt;h3&gt;
  
  
  Options page
&lt;/h3&gt;

&lt;p&gt;Here are some features on the options page Kiro generated for me, including task management and distraction management, where you add sites to your "block list":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task Management Tab&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmipntykfxdxhyfq74ldc.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%2Fmipntykfxdxhyfq74ldc.png" alt="MVP task management"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distraction Management Tab&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwuanynzkti0xgv9hjxw7.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%2Fwuanynzkti0xgv9hjxw7.png" alt="MVP distraction management"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statistics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flc9g4zzsz0f69biw8nf0.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%2Flc9g4zzsz0f69biw8nf0.png" alt="MVP Statistics"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Theme Change
&lt;/h3&gt;

&lt;p&gt;Here's the popup after I unlocked a theme and applied it, just to show it worked:&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%2Fevd3piwn7ovcjg9aw445.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%2Fevd3piwn7ovcjg9aw445.PNG" alt="MVP changing theme"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are a bunch of other tabs in the options page, but I'll skip those just because it's getting long.&lt;/p&gt;

&lt;p&gt;Scroll down to see the comparison to the final product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Features with Kiro
&lt;/h2&gt;

&lt;p&gt;I was pretty impressed already by the first iteration of my extension Kiro produced. I had used vibe mode to fix bugs and make minor adjustments.&lt;/p&gt;

&lt;p&gt;But let's go beyond that. As in real projects you're unlikely to be done with just one iteration, no matter how flawlessly it went.&lt;/p&gt;

&lt;p&gt;Let's add a feature. I wanted to calibrate reward and boss damage calculations (Soul Embers and Boss Damage) based on the initial algorithm Kiro gave me. I wanted to easily check how much I earn through idling and through boss fights at different player and skill levels without having to actually level up-- a way to simulate player status and circumstance. Because doing it manually while waiting for focus sessions would take ETERNITY!&lt;/p&gt;

&lt;p&gt;So I went into spec mode again and asked Kiro to make this feature for me, and to add a new spec. Because this feature is big enough for me to want to be thorough. &lt;/p&gt;

&lt;p&gt;I asked it to create a "dev screen", separate from the extension, which can simulate player levels and skills and allow me to see how many rewards and how much damage would a player at any level or skills would get and do without having to do actual focus sessions.&lt;/p&gt;

&lt;p&gt;I also specifically asked it to draw from the same sources and functions as the actual extension, so everything kept consistency, and so Kiro would only need to test in one place.&lt;/p&gt;

&lt;p&gt;Of course, it generated the appropriate spec documents for me. I then subsequently ran all the tasks so Kiro could do it's thing.&lt;/p&gt;

&lt;p&gt;Here's what it made me:&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%2F50hhyswymog485f3uwxf.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%2F50hhyswymog485f3uwxf.png" alt="Dev Screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this time it worked straight away. It gave me a script to build and run it at port 3000.&lt;/p&gt;

&lt;p&gt;Again, this proves that Kiro works beyond just initial spec generation and implementation. You could use this on existing projects to add new features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Captaining the Creative Ship with Kiro Vibe Code
&lt;/h2&gt;

&lt;p&gt;This is the most precious part for me. Sometimes I really enjoy the creative process more than the engineering part. And Kiro allowed me to focus on that creative part!&lt;/p&gt;

&lt;p&gt;And in this particular project, I really think I came up with a concept and theme that I seriously enjoy and am proud of! It's simple but coherent, and I like the terminologies I came up with.&lt;/p&gt;

&lt;p&gt;Thanks to Kiro, I could keep expanding upon the initial concept of "soul collectiong" while it translated my ideas into working code. While it worked on my code, I could let my mind travel back to the concept and come up with new stuff. And I did!&lt;/p&gt;

&lt;p&gt;The boss bestiary or gallery screen was something I got the idea for while Kiro was doing its thing running tests for me!&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Product
&lt;/h2&gt;

&lt;p&gt;I won't go into details for features already established in the MVP section, but I'll explain any additions I requested Kiro beyond the first iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Main Popup UI
&lt;/h3&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%2Fwehheladlh7uksxmt4pv.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%2Fwehheladlh7uksxmt4pv.png" alt="Final Popup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Popup During Focus Session
&lt;/h3&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%2Fhe5fe3y2o15jw5xgqfwq.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%2Fhe5fe3y2o15jw5xgqfwq.PNG" alt="Final popup during focus session"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discouraged and Blocked Sites
&lt;/h3&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%2Fyy429mqha8e9ratxnudt.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%2Fyy429mqha8e9ratxnudt.png" alt="Visiting discouraged site in final product"&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%2Frgwitz9ibcj2n5r5z00t.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%2Frgwitz9ibcj2n5r5z00t.png" alt="Visiting blocked site in final product"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Break Time
&lt;/h3&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%2Fmpioztu5zjvyfy6r4wbd.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%2Fmpioztu5zjvyfy6r4wbd.png" alt="Final break time UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Options page
&lt;/h3&gt;

&lt;p&gt;Here's something interesting: In the statistic options tab, I asked Kiro to add a player card feature. Something concise and pretty to show off current progress using spec mode.&lt;/p&gt;

&lt;p&gt;At first it designed the card on its own, as I didn't give it any direction. It looked nice enough, but not what I envisioned.&lt;/p&gt;

&lt;p&gt;So I drew a simple design on MS Paint and gave it to Kiro since you can paste pictures there.&lt;/p&gt;

&lt;p&gt;Here was the design:&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%2Fwhn6rwkziqjla2ukk32n.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%2Fwhn6rwkziqjla2ukk32n.png" alt="Card design"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And finally after a few vibe code back-and-forth, here's what I ended up with:&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%2Fatiylm5imrwltf45xo1s.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%2Fatiylm5imrwltf45xo1s.PNG" alt="Player Card"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not bad, eh?&lt;/p&gt;

&lt;h3&gt;
  
  
  Boss Bestiary or Stubborn Soul Gallery
&lt;/h3&gt;

&lt;p&gt;And finally before I published I had a quick idea I wanted Kiro to implement. Most games with a variety of bosses always have a gallery of sorts you can view to look back on your progress and the road ahead. Here it is:&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%2F8efjk5kd9cdgvwkrywr6.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%2F8efjk5kd9cdgvwkrywr6.png" alt="Boss Bestiary"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A list of bosses you've unlocked, guided (defeated), and ones you'll get to unlock after leveling up.&lt;/p&gt;

&lt;p&gt;When you click on an unlocked boss/Stubborn Soul:&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%2Ftc3z39mhzmc2wy84ibob.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%2Ftc3z39mhzmc2wy84ibob.PNG" alt="Boss profile"&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%2Fk2lb9o69zm62kbsa2r5d.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%2Fk2lb9o69zm62kbsa2r5d.PNG" alt="Locked boss content"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To give player more motivation and direction, I added locked content for each boss for players to reveal once they've "defeated" each Stubborn Soul:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Final Conversation: A heartfelt dialogue where the Soul Shepherd helps each Stubborn Soul find peace and acceptance before they move on. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resolution: A narrative epilogue that describes how the soul finally finds peace and moves on.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example just for the first boss; I don't want to spoil any more:&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%2Fmdxhcvdwgw73svlag6uq.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%2Fmdxhcvdwgw73svlag6uq.PNG" alt="Boss content unlocked"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I definitely see myself using Kiro beyond the challenge context, especially when I want to focus on concept more than implementation. I also would like to try integrating MCPs into Kiro. Next project! Maybe I'll integrate it with my local database so I don't have to explain schema structures to it-- a pain point I'm way too familiar with when it comes to code agents.&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>ai</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Use Appwrite in Android Jetpack Compose</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Thu, 24 Jul 2025 04:47:26 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/how-to-use-appwrite-in-android-jetpack-compose-j0l</link>
      <guid>https://forem.com/ansellmaximilian/how-to-use-appwrite-in-android-jetpack-compose-j0l</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I love &lt;a href="https://appwrite.io/" rel="noopener noreferrer"&gt;Appwrite&lt;/a&gt;. My first hackathon was actually from Appwrite (using Appwrite) 2 years ago, and I've been using it ever since.&lt;/p&gt;

&lt;p&gt;It has basically every basic feature that you could want for a &lt;em&gt;backend as a service&lt;/em&gt; and serves as a perfect base for almost any kind of app you could want to create.&lt;/p&gt;

&lt;p&gt;It has a very generous free tier and therefore is really good for prototyping app ideas. And when you're ready for production, you can easily upgrade!&lt;/p&gt;

&lt;p&gt;However, I've only been using it for creating web applications. Recently I've started to take interest in mobile development, specifically using  &lt;a href="https://developer.android.com/jetpack?gad_source=1&amp;amp;gad_campaignid=21831783573&amp;amp;gbraid=0AAAAAC-IOZkUrWhLnvmkJFVBI2J34hCP2&amp;amp;gclid=CjwKCAjw7fzDBhA7EiwAOqJkh6zG6Fu5TNsdfndT50_mqB-7uOADr8E7jIGTnl1wvfpbEE38lVCHuRoCtIUQAvD_BwE&amp;amp;gclsrc=aw.ds" rel="noopener noreferrer"&gt;Jetpack Compose&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I thought Appwrite would be perfect to help me on my learning journey. I would have a ready backend and can just focus on learning the fundamentals of Jetpack Compose Android development.&lt;/p&gt;

&lt;p&gt;That's why I made this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is This For?
&lt;/h2&gt;

&lt;p&gt;Ultimately, this is for my future self and anyone else who might want a quick overview of how to integrate Appwrite into a Jetpack Compose Android application.&lt;/p&gt;

&lt;p&gt;Note that I'm writing this as a beginner, but I will try to make sure all the info are objectively accurate.&lt;/p&gt;

&lt;p&gt;At the end, I aim to have an article that I can look back to and get back a basic understanding fairly quickly when I'm rusty.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Is Going To Work
&lt;/h2&gt;

&lt;p&gt;The tutorial is going to be heavily based on this &lt;a href="https://appwrite.io/docs/quick-starts/android" rel="noopener noreferrer"&gt;quick start guide by Appwrite&lt;/a&gt; with a few adjustments.&lt;/p&gt;

&lt;p&gt;For each new major Jetpack Compose/Android concept we encounter, I will explain it clearly within the current context without digressing too deep into other topics.&lt;/p&gt;

&lt;p&gt;At the end, we will have the same result as the original article, which is a working &lt;em&gt;Login Page&lt;/em&gt; that uses Appwrite. Spoiler:&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%2Fc42vg550txh3phtkkftg.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%2Fc42vg550txh3phtkkftg.png" alt=" " width="540" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, I won't just be regurgitating the same info as the original article: I will be explaining each concept clearly based on my objective understanding to add value to this article.&lt;/p&gt;

&lt;p&gt;As a side note, it would definitely help if you came from a web background (like me), because I use a lot of analogies comparing this to web development, especially front end.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Android Studio&lt;/li&gt;
&lt;li&gt;Basic Programming Knowledge&lt;/li&gt;
&lt;li&gt;Appwrite Account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Integrating Appwrite
&lt;/h2&gt;

&lt;p&gt;Let's actually get into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Android Project
&lt;/h3&gt;

&lt;p&gt;If you'd rather clone a starting project, here's a &lt;a href="https://github.com/AnsellMaximilian/app-post-mobile/tree/init" rel="noopener noreferrer"&gt;link to my repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In Android Studio, create a new project. When given the prompt to choose a template, choose "Empty Activity".&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%2Fu9rvbrjnbgdemajxc4x5.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%2Fu9rvbrjnbgdemajxc4x5.png" alt="Project Template Prompt in Android Studio" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Name the application whatever you want, but pay attention to the "Package name", as this will be used somewhere else later. Leave the other settings as the defaults:&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%2Fp1nu2cq1lyny7wbakr9u.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%2Fp1nu2cq1lyny7wbakr9u.png" alt="Project Creation" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Appwrite Project
&lt;/h3&gt;

&lt;p&gt;Go to the &lt;a href="https://cloud.appwrite.io/" rel="noopener noreferrer"&gt;Appwrite Console&lt;/a&gt; and create a new project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a New Android Platform
&lt;/h3&gt;

&lt;p&gt;This is actually a very important step. Just like if you were creating a web app -- where you'd need to create a web platform and add allowed domains, you need to add an Android platform and fill in the appropriate details from your actual Android app, namely the package name (I've found that the  "Name" is irrelevant and that you can fill it with whatever you want).&lt;/p&gt;

&lt;p&gt;Without this step (or by filling in the wrong package name), you'd get an &lt;em&gt;invalid origin&lt;/em&gt; error.&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%2Fjw1e9cjypgofk0mbxqrc.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%2Fjw1e9cjypgofk0mbxqrc.png" alt="Adding Android Platform" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Dependency
&lt;/h3&gt;

&lt;p&gt;Just like you'd use NPM libraries inside your React projects, you can add existing libraries here in your Android project. Specifically, you can add the Appwrite SDK as part of your project so you can use existing code that the Appwrite team wrote to easily communicate with your project.&lt;/p&gt;

&lt;p&gt;There are two &lt;code&gt;build.gradle.kts&lt;/code&gt; files in your Android project, but you need to add the dependency to the one in the app level: &lt;code&gt;&amp;lt;your project&amp;gt;/app/build.gradle.kts&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;implementation("io.appwrite:sdk-for-android:8.1.0")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At time of writing, this is the latest version of the Appwrite SDK, but if you ever want to use the latest version, look it up in the &lt;a href="https://mvnrepository.com/artifact/io.appwrite/sdk-for-android" rel="noopener noreferrer"&gt;respective library page&lt;/a&gt; where it is hosted in the Maven Repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Appwrite Singleton
&lt;/h3&gt;

&lt;p&gt;In the same level as &lt;code&gt;MainActivity.kt&lt;/code&gt;, create a new Kotlin file and call it... whatever you want. I'll call mine &lt;code&gt;Appwrite.kt&lt;/code&gt;. Fill it with the following:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.content.Context&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.appwrite.Client&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.appwrite.ID&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.appwrite.models.*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.appwrite.services.*&lt;/span&gt;

&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Appwrite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;
    &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Account&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;YOUR_PROJECT_ENDPOINT&amp;gt;)
&lt;/span&gt;            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;YOUR_PROJECT_ID&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createEmailPasswordSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;password&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;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;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="n"&gt;account&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="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;password&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;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onLogout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"current"&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 Kotlin, you create singletons using the &lt;code&gt;object&lt;/code&gt; keyword. You create a singleton because you'd never need another instance of this object ever again.&lt;/p&gt;

&lt;p&gt;Once it's initialized, it will be available anywhere, and you'd never need to reinitialize it again.&lt;/p&gt;

&lt;p&gt;Appwrite separates its APIs by services. Here, &lt;code&gt;Client&lt;/code&gt; is the core or foundation that all services use. This makes sure the service actually communicates with the right project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Account&lt;/code&gt; is one of Appwrite's many services, used for handling authentication.&lt;/p&gt;

&lt;p&gt;So, in summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You create a singleton called &lt;code&gt;Appwrite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You give it two properties (&lt;code&gt;client&lt;/code&gt; and &lt;code&gt;account&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Those two properties have the &lt;code&gt;lateinit&lt;/code&gt; modifier: &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We use &lt;code&gt;lateinit&lt;/code&gt; because we are defining the properties as "non nullable". Meaning it has to have a value: instance of &lt;code&gt;Client&lt;/code&gt; and &lt;code&gt;Account&lt;/code&gt; respectively.&lt;/p&gt;

&lt;p&gt;We could make it nullable by doing something like &lt;code&gt;var client: Client? = null&lt;/code&gt;. But then we'd have to do null checks every time we want to use it: &lt;code&gt;client?.doSomething()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So a compromise is &lt;code&gt;lateinit&lt;/code&gt;: we are basically saying to the compiler: "Listen, I promise you it's not going to be &lt;code&gt;null&lt;/code&gt; when I use it. I'll initialize it first. This won't be null." The error then goes away.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why can't you just initialize it immediately in the singleton?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the Appwrite client requires &lt;code&gt;Context&lt;/code&gt;, which you can't get inside the singleton. It needs to be provided where context is available, like activities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We create suspended functions &lt;code&gt;onLogin&lt;/code&gt;, &lt;code&gt;onRegister&lt;/code&gt;, and &lt;code&gt;onLogout&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's suspended because the code inside it is asynchronous, i.e. registering a user. You need to add &lt;code&gt;suspended&lt;/code&gt; because you call other other suspended functions.&lt;/p&gt;

&lt;p&gt;This also allows you to &lt;code&gt;await&lt;/code&gt; these functions and perform actions based on the timing of its execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onLogout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;showSnackbar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Logged out successfully"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showSnackbar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to log out: ${e.message}"&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 way you only show the snackbar ONCE you've actually logged out.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inside these defined functions, you call the account service's built-in methods, provided by Appwrite.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create Login Page
&lt;/h3&gt;

&lt;p&gt;The template we choose earlier provided a MainActivity. As declared in the &lt;code&gt;AndroidManifest.xml&lt;/code&gt; file, this will be the activity loaded when you build and run the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;activity&lt;/span&gt;
    &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".MainActivity"&lt;/span&gt;
    &lt;span class="na"&gt;android:exported=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
    &lt;span class="na"&gt;android:label=&lt;/span&gt;&lt;span class="s"&gt;"@string/app_name"&lt;/span&gt;
    &lt;span class="na"&gt;android:theme=&lt;/span&gt;&lt;span class="s"&gt;"@style/Theme.AppPost"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;intent-filter&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.action.MAIN"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;category&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.LAUNCHER"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/activity&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll replace the default activity with the following to create a login page:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.os.Bundle&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.widget.Toast&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.activity.ComponentActivity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.activity.compose.setContent&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.activity.enableEdgeToEdge&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.Arrangement&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.Column&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.Row&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.fillMaxSize&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.fillMaxWidth&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.padding&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.text.KeyboardOptions&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.material3.Button&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.material3.MaterialTheme&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.material3.Scaffold&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.material3.Surface&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.material3.Text&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.material3.TextField&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.Composable&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.LaunchedEffect&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.getValue&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.mutableStateOf&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.remember&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.rememberCoroutineScope&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.setValue&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.Alignment&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.Modifier&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.platform.LocalContext&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.text.input.KeyboardType&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.text.input.PasswordVisualTransformation&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.tooling.preview.Preview&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.unit.dp&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;your.package.name.ui.theme.AppPostTheme&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.launch&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ComponentActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;enableEdgeToEdge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nc"&gt;Appwrite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;applicationContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;setContent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;AppPostTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;innerPadding&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="nc"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerPadding&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;coroutineScope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberCoroutineScope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;var&lt;/span&gt; &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;var&lt;/span&gt; &lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;LaunchedEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Appwrite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
               &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                   &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LENGTH_SHORT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;horizontalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterHorizontally&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;verticalArrangement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arrangement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Logged in as $user"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;Appwrite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onLogout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&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="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Logout"&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;horizontalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterHorizontally&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;verticalArrangement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arrangement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;onValueChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nc"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;onValueChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;visualTransformation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PasswordVisualTransformation&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;keyboardOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyboardType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;horizontalArrangement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arrangement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SpaceBetween&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="nc"&gt;Appwrite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                                &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;

                            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LENGTH_SHORT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;Text&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;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="nc"&gt;Appwrite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;printStackTrace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                                &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LENGTH_SHORT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Register"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;



&lt;p&gt;Like I said, I won't digress too far into other topics and I'll stick to what's on the code. For example, I won't go into other activity cycles like &lt;code&gt;onDestroy&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;In summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We override the &lt;code&gt;onCreate&lt;/code&gt; lifecycle method. It's the first one called among the others, which makes it perfect for initializations (like our Appwrite singleton)&lt;/li&gt;
&lt;li&gt;Since we override &lt;code&gt;onCreate&lt;/code&gt;, you want to call the original &lt;code&gt;onCreate&lt;/code&gt; to make sure everything works as intended &lt;code&gt;super.onCreate(savedInstanceState)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Then you initialize the Appwrite singleton with &lt;code&gt;Appwrite.init(applicationContext)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the perfect place for initializing our singleton because in modern  Jetpack Compose apps, there usually only is one activity. And &lt;code&gt;onCreate&lt;/code&gt; is the first lifecycle that's called.&lt;/p&gt;

&lt;p&gt;This means that, for this simple app, you likely won't be calling the singleton's methods before it's initialized.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You provide the &lt;code&gt;init&lt;/code&gt; method with &lt;code&gt;applicationContext&lt;/code&gt;:
&lt;code&gt;applicationContext&lt;/code&gt; inherits from &lt;code&gt;Context&lt;/code&gt;, which provides app specific information. Appwrite likely needs this for using &lt;code&gt;SharedPreferences&lt;/code&gt; (similar to local storage in the web) for storing auth tokens. &lt;code&gt;Context&lt;/code&gt; allows Appwrite to find where this is located for your specific app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other things can have &lt;code&gt;Context&lt;/code&gt; as well, but &lt;code&gt;applicationContext&lt;/code&gt; is safe because it lasts the whole lifetime of the app, as opposed to an Activity's &lt;code&gt;Context&lt;/code&gt; which may be destroyed during the run of the app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;setContent&lt;/code&gt; is where you "draw" your page/activity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AppPostTheme&lt;/code&gt; wraps around everything to provide the theme of the app, like primary color, background color, etc. This will be called different things depending on what you named your app. An app named "Super App" will have &lt;code&gt;SuperAppTheme&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You create the &lt;code&gt;LoginPage&lt;/code&gt; composable&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;@Composable&lt;/code&gt; annotation marks it as a composable. This means that the function will "describe your UI". Composables are where you "write" your UI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;LoginPage&lt;/code&gt; first calls &lt;code&gt;Surface&lt;/code&gt;, a built-in composable. You provide it with &lt;code&gt;MaterialTheme.colorScheme.background&lt;/code&gt;, the value of which will be determined by your theme.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You get the local context using &lt;code&gt;LocalContext.current&lt;/code&gt;. This will resolve to the current Activity's &lt;code&gt;Context&lt;/code&gt; as that's where login page is called&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You create a coroutine scope: &lt;code&gt;val coroutineScope = rememberCoroutineScope()&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coroutines are where you write asynchronous code. In this specific case, inside the composable, you use &lt;code&gt;rememberCoroutineScope&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This allows running asynchronous code to survive recompositions. That's when the composable redraws itself after state change.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You declare states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ooh boy this took a while to fully understand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;var&lt;/span&gt; &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;var&lt;/span&gt; &lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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 use &lt;code&gt;var&lt;/code&gt; because you want to reassign these at some point.&lt;/p&gt;

&lt;p&gt;You use &lt;code&gt;mutableStateOf&lt;/code&gt; so that it will trigger recomposition after change. (Making sure UI updates after state change).&lt;/p&gt;

&lt;p&gt;You use &lt;code&gt;remember&lt;/code&gt; to make sure the state change survives said recomposition (within the same composition).&lt;/p&gt;

&lt;p&gt;You use &lt;code&gt;by&lt;/code&gt; because without it &lt;code&gt;mutableStateOf&lt;/code&gt; would've required you to use &lt;code&gt;variable.value&lt;/code&gt; instead of just &lt;code&gt;variable&lt;/code&gt; to access the state.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You use &lt;code&gt;LaunchedEffect&lt;/code&gt; to check if you have an active session after the app is run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;LaunchedEffect&lt;/code&gt; is a side-effect handler that gets executed every time its dependencies change.&lt;/p&gt;

&lt;p&gt;In this case, there are no dependencies so you put &lt;code&gt;Unit&lt;/code&gt; in it. So the code will only get executed once during the first composition.&lt;/p&gt;

&lt;p&gt;You use it because a user might have an active session, so you get the current user and if they exist, you assign their email to &lt;code&gt;email&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You also use &lt;code&gt;coroutineScope.launch&lt;/code&gt; to make sure the fetching of the current user doesn't get interrupted by recomposition, i.e. if, while fetching, the user updates the input values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conditional UI rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You use an &lt;code&gt;if&lt;/code&gt; &lt;code&gt;else&lt;/code&gt; statement to render different UI based on whether or not a user is logged in (whether the &lt;code&gt;email&lt;/code&gt; state is empty or not)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composables (&lt;code&gt;Column&lt;/code&gt;, &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;Button&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You arrange your UI by calling composable functions. Some composables like &lt;code&gt;Column&lt;/code&gt; take in other composables and arrange them in certain ways.&lt;/p&gt;

&lt;p&gt;Some composables directly render UI and add interactivity, like &lt;code&gt;Button&lt;/code&gt; and &lt;code&gt;Text&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Modifier&lt;/code&gt; is used to style a composable. Like the &lt;code&gt;style&lt;/code&gt; property in HTML or React elements.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using &lt;code&gt;coroutineScope&lt;/code&gt; to run suspended functions in the Appwrite singleton&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you were following along, after creating the template project with an empty activity, you should have done these things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created the Appwrite singleton in &lt;code&gt;Appwrite.kt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Modified the &lt;code&gt;MainActivity.kt&lt;/code&gt; file to create a login page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you run the app, you should have something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvywlzxnkjuncvehpwlw.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%2Fnvywlzxnkjuncvehpwlw.png" alt=" " width="540" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you register an account and subsequently log in, the login form will disappear and be replaced with this (thanks to conditional rendering):&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%2Fao7xaiaetswd4lsvuatj.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%2Fao7xaiaetswd4lsvuatj.png" alt=" " width="540" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the &lt;a href="https://github.com/AnsellMaximilian/app-post-mobile/tree/add-appwrite" rel="noopener noreferrer"&gt;final code&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>androiddev</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Office Space: Actual Office Space</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Fri, 18 Jul 2025 09:28:59 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/office-space-actual-office-space-23d4</link>
      <guid>https://forem.com/ansellmaximilian/office-space-actual-office-space-23d4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend/axero"&gt;Frontend Challenge: Office Edition sponsored by Axero, Holistic Webdev: Office Space&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I went goofy and very literal for this one. Please don't take it super seriously, but I do want to provide some value to this article. &lt;/p&gt;

&lt;p&gt;The challenge was to create a home page within the themes of "office". So, I decided to attempt to create an actual office space (desk, wall, folders, etc.) and make IT the home page. &lt;/p&gt;

&lt;p&gt;I tried combining the art with the features (brand, navigation, upcoming events) as seamlessly as possible within the represented items inside the office (laptop, folders, desk, etc.).&lt;/p&gt;

&lt;p&gt;Disclaimer: I am not an artist so excuse the quality of the "drawings", but the idea is there... and hopefully you can see it. Also, I didn't make it responsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Home Page
&lt;/h3&gt;

&lt;p&gt;Before showing you the demo, here's a quick list of what I actually included in the "office":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laptop: for displaying promotional video of the company&lt;/li&gt;
&lt;li&gt;Folders: main navigation where each labeled folder redirects users to a different page&lt;/li&gt;
&lt;li&gt;Calendar: an actual calendar that lists the company's event for that particular month&lt;/li&gt;
&lt;li&gt;Trophy: lists the company's achievements or the products features. &lt;/li&gt;
&lt;li&gt;Banner: the company logo and name&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TailwindCSS&lt;/li&gt;
&lt;li&gt;Shadcn&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Easter Egg
&lt;/h3&gt;

&lt;p&gt;Like I said I'd like to provide a little value to readers even though this is just a fun and goofy post. &lt;/p&gt;

&lt;p&gt;Without watching the demo video or looking at the code, see if you can find the Easter egg within the main page. &lt;/p&gt;

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

&lt;h3&gt;
  
  
  Quick Video: Easter Egg Spoilers
&lt;/h3&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/1u3SymyIgtA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/AnsellMaximilian/office-homepage" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://office-homepage.netlify.app/" rel="noopener noreferrer"&gt;Live Page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Home page
&lt;/h4&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%2F5rx4s0pqxfj3lo74dubc.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%2F5rx4s0pqxfj3lo74dubc.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My favorite part is definitely the lamp. I made the bulb and covered the whole page with that glow. Then asked ChatGPT to create a cone shape with polygon clipping.&lt;/p&gt;

&lt;h4&gt;
  
  
  Laptop: Promotional Video
&lt;/h4&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%2Fb8tgq1f371r99sox4wur.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%2Fb8tgq1f371r99sox4wur.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where I played around with perspective and rotation to give the illusion of 3D space. I think it worked out really well.&lt;/p&gt;

&lt;h4&gt;
  
  
  Folders: Navigation
&lt;/h4&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%2Fox2ioa7rgaxf67ec1xv2.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%2Fox2ioa7rgaxf67ec1xv2.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm most proud of this one. They really look like folders. I wish I knew more about creating 3D art with CSS to make this pop out more, but this was good enough for me.&lt;/p&gt;

&lt;h4&gt;
  
  
  Calendar: Upcoming Events
&lt;/h4&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%2Fxyhg1zhuq9vl4elgouo4.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%2Fxyhg1zhuq9vl4elgouo4.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty simple. You click on a date with an event(s), and you'll see a Post-It note of the details.&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%2F2361nquee7j8dz0g84cw.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%2F2361nquee7j8dz0g84cw.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Trophy: Features/Achievements
&lt;/h4&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%2Fmedvm4umhed8y97yi68b.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%2Fmedvm4umhed8y97yi68b.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This could've done better, but it was pretty good for a first attempt.&lt;/p&gt;

&lt;h4&gt;
  
  
  Banner: Company Name and Logo
&lt;/h4&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%2Fdbwc8cuhlg8cmkh3q2uy.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%2Fdbwc8cuhlg8cmkh3q2uy.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I really like the nail heads, especially the shine effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;I don't remember the last time trying something silly like this. But I had a lot of fun, and it gave me a lot more appreciation for people who are actually good at making web art.&lt;/p&gt;

&lt;p&gt;I re-learned a lot of things that I had barely used lime &lt;code&gt;skew&lt;/code&gt; and &lt;code&gt;perspective&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Overall it was just a fun experience. And coming up and developing the Easter egg was also super exciting. &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>css</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Can Gemini Figure Out the Ultimate Question: 100 Men Versus a Gorilla — Who Would Win?</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Wed, 09 Jul 2025 07:04:08 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/can-gemini-figure-out-the-ultimate-question-100-men-versus-a-gorilla-who-would-win-475h</link>
      <guid>https://forem.com/ansellmaximilian/can-gemini-figure-out-the-ultimate-question-100-men-versus-a-gorilla-who-would-win-475h</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is my submission for &lt;a href="https://dev.to/deved/build-apps-with-google-ai-studio"&gt;DEV Education Track: Build Apps with Google AI Studio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spoiler: I absolutely love this new feature in Gemini. It works awesome. I've tried many different prompts for many different app ideas, and in the end I always end up with a working app that looks great, handles errors, is well-organized, and does exactly what I want.&lt;/p&gt;

&lt;p&gt;You can test out the final app &lt;a href="https://aistudio.google.com/app/prompts?state=%7B%22ids%22:%5B%221fiP_FTqCMhu33mfRSYlyoPoEf_w_EjTr%22%5D,%22action%22:%22open%22,%22userId%22:%22114304486110190708566%22,%22resourceKeys%22:%7B%7D%7D&amp;amp;usp=sharing" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;It was super big a while ago over on X/Twitter: "Can 100 men beat one gorilla?"&lt;/p&gt;

&lt;p&gt;Most of the participants in this discussion didn't have any expertise to answer one or the other AND explain it accurately.&lt;/p&gt;

&lt;p&gt;That's how I got the idea for this app. Why not let AI decide. It's got all the information and is really good at reasoning.&lt;/p&gt;

&lt;p&gt;The basic premise of the app is to simulate a battle between two teams scientifically. So users will be able to pit two teams comprising of one or more characters (real or fictional) and see who the AI thinks would win, based on real world metrics.&lt;/p&gt;

&lt;p&gt;Though any characters is valid, the driving motivation to create this app is to finally and reliably answer that question: 100 men vs one gorilla — who would win?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Main Prompt
&lt;/h3&gt;

&lt;p&gt;I started with the main prompt to get the basic features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user inputs what the two teams are&lt;/li&gt;
&lt;li&gt;AI simulates the result&lt;/li&gt;
&lt;li&gt;display the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Besides describing the main features, I also sprinkled in some commands for Gemini to include some support features and specific styling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error handling: I asked it to elegantly handle any errors and distinguish between all types of possible errors&lt;/li&gt;
&lt;li&gt;Science based description: I asked it to analyze the battle scientifically and apply real-world metrics like &lt;code&gt;km/h&lt;/code&gt;, &lt;code&gt;tons&lt;/code&gt;, &lt;code&gt;voltage&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Styling: I asked it to theme the application around the concept of "Red vs Blue".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here was the full prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create an app called "Who Would Win".  

It takes two teams (each comprising of a character OR groups of characters) by two fields: name and description.

Then use Gemini to simulate a realistic battle to the end and generate the result.

The result should describe the battle in scientific terms. 

You should map supernatural/fictional powers as close to real world metrics as you can. For example: if a character in lore somehow shoots lasers out of their eyes; explain how much heat it generates, how destructive it is, etc.

Theme the whole app around red vs. blue.

If a character is non-existent and the description is too vague or also doesn't exist, display an error. For a battle to be valid, the teams need to comprise of either recognizable characters OR created characters with enough description.

Make sure to differentiate between Gemini API error and invalid teams error.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the result in more detail below, but a quick review:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gemini absolutely crushed it — as expected after testing it out a bunch. The main features were implemented beautifully in a responsive UI. The support features and styling requests were also interpreted and implemented correctly. The "Red vs. Blue" style was done elegantly without making the app look corny.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Enhancements/Adjustments with Gemini
&lt;/h3&gt;

&lt;p&gt;After a satisfying initial prompting, I decided to test its adjustment/enhancements capabilities. Because generating a project and enhancing/adjusting it, to me, seems intuitively different enough that I needed to test it.&lt;/p&gt;

&lt;p&gt;The first prompt didn't include image generation. Let's try adding that. Here is the prompt I entered into the code assistant panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aside from text result, also use Imagen to generate an image of the battle result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the result will be shown in more detail below. But to summarize: it worked really great. I expected an adjustment process like this to mess up existing code or at least change it very noticeably. But to my surprise, the UI and the previous features stayed largely the same.&lt;/p&gt;

&lt;p&gt;It added the new requested feature very seamlessly and correctly.&lt;/p&gt;

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

&lt;p&gt;Here's a video clip of my whole process of developing this whole app. Generating the initial project took about ~2.5 minutes, while the adjustments took about ~1.5 minutes.&lt;/p&gt;

&lt;p&gt;The video doesn't contain any editing in an attempt to show you in real time the whole process of creating this app. Feel free to skip around:&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/A8mejTlg4iQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here's what the final app looks like:&lt;br&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%2Fvg7dghody86nhlb6wcym.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%2Fvg7dghody86nhlb6wcym.png" alt="Final App"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience
&lt;/h2&gt;

&lt;p&gt;Here's a detailed chronology of my experience using Gemini's build feature, from beginning to end:&lt;/p&gt;

&lt;h3&gt;
  
  
  First Prompt
&lt;/h3&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%2F9quzwaoqak6mlqcjzgu5.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%2F9quzwaoqak6mlqcjzgu5.png" alt="First Prompt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty straightforward process. Please note that this was my first iteration of the prompt. I didn't enhance it using another AI or anything. I wanted to push Gemini to see if how much prompt engineering skills you need to produce satisfying results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Result of First Prompt
&lt;/h3&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%2Fxbbaou29ubqcu76giobx.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%2Fxbbaou29ubqcu76giobx.png" alt="First Prompt Result"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Turns out you don't! Obviously, you can't just write gibberish and expect it to understand. But this shows that Gemini was able to interpret what I wanted from a simple and human prompt.&lt;/p&gt;

&lt;p&gt;Everything I described in the prompt was implemented correctly and more.&lt;/p&gt;

&lt;h4&gt;
  
  
  Error handling
&lt;/h4&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%2Fwaeokf9ps52jc408al1x.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%2Fwaeokf9ps52jc408al1x.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Error is handled very beautifully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Out the App
&lt;/h3&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%2Fsg33zsrvre83be5paa8c.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%2Fsg33zsrvre83be5paa8c.png" alt="Battle Result"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Created Files
&lt;/h3&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%2Fon1bffvm62hf1c14nptn.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%2Fon1bffvm62hf1c14nptn.png" alt="Created Files"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the current version of the app. I finally got to simulate the battle between 100 men and a gorilla. 100 men won!&lt;/p&gt;

&lt;p&gt;I was super happy with the result, as I was always team "100 men".&lt;/p&gt;

&lt;p&gt;As you can see, there is detailed, grounded, and scientific explanation of the fight with real-world metrics like strength measured in &lt;code&gt;N&lt;/code&gt;, velocity, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enhancement and Adjustment
&lt;/h3&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%2Fqgfimsuzx4ct3iqkgh16.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%2Fqgfimsuzx4ct3iqkgh16.png" alt="Adjustment Prompt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where I tested Gemini's adjustment/enhancement capabilities. Currently the app doesn't generate the image representing the battle. So I went over to the code assistant panel and added a new prompt, requesting it to add — on top of the existing features — an in image generation feature using Imagen.&lt;/p&gt;

&lt;p&gt;It worked really well. What's amazing is that the previous code not relevant to the new feature stayed very largely the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adjustment Result
&lt;/h3&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%2Fzi8q5zv6vor8k1lo4tdb.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%2Fzi8q5zv6vor8k1lo4tdb.png" alt="Adjustment Result"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the resulting app after the enhancement. As you can see, the styling stayed the same, but now there's an image there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Result
&lt;/h3&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%2Fk53ubd8ueitwko5me9nt.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%2Fk53ubd8ueitwko5me9nt.png" alt="Result with Image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the app in full screen. Looks great!&lt;/p&gt;

</description>
      <category>deved</category>
      <category>learngoogleaistudio</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Putting Runner H Through the Gauntlet: Ongoing Hackathon Weekly Digest</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Sat, 05 Jul 2025 11:35:05 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/putting-runner-h-through-the-gauntlet-ongoing-hackathon-weekly-digest-kgp</link>
      <guid>https://forem.com/ansellmaximilian/putting-runner-h-through-the-gauntlet-ongoing-hackathon-weekly-digest-kgp</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/runnerh"&gt;Runner H "AI Agent Prompting" Challenge&lt;/a&gt;&lt;/em&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%2F2vj03o5zpqpabd9phu9m.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%2F2vj03o5zpqpabd9phu9m.png" alt="Runner H illustration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I want to approach this differently. I was super interested in what seemed like ChatGPT with a ton of capabilities and access to external apps. So I wanted to share my journey of learning what &lt;strong&gt;Runner H&lt;/strong&gt; is capable of.&lt;/p&gt;

&lt;p&gt;So, in this article, I will be putting &lt;strong&gt;Runner H&lt;/strong&gt; through a series of challenges with increasing difficulty to test its capabilities.&lt;/p&gt;

&lt;p&gt;Of course, I'm not the best prompt engineer, so take my results with a grain of salt. I encourage you to try &lt;strong&gt;Runner H&lt;/strong&gt; yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spoiler: Final Result - A weekly, newspaper-style digest of currently active hackathons, curated by an AI agent.
&lt;/h3&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%2F8zxutrgaz2h20g7hsqea.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%2F8zxutrgaz2h20g7hsqea.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The main goal of this article is to test the capabilities &lt;strong&gt;AND&lt;/strong&gt; limitations of RunnerH. But I will have a general direction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fetching the latest information of the currently ongoing hackathons/challenges and organizing them&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  Quick Video Demo
&lt;/h3&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_GqcM4xX5ac"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In this section is where I'll be testing out RunnerH towards that general goal I've set it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Round 1: Fetching Current Data
&lt;/h3&gt;

&lt;p&gt;First, let's see how well RunnerH can fetch the most current data about ongoing hackathons. Let's limit it to a few platforms, namely Devpost.com and Dev.to (you are here).&lt;/p&gt;

&lt;p&gt;Here's the first prompt I tried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Generate a list of currently ongoing hackathons happening at Dev.to, Devpost.com, and Hashnode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here was the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I searched the platforms and here's what I found about ongoing hackathons:

1. **Dev.to**: There are no direct ongoing hackathon listings found.
2. **Devpost.com**: Several ongoing hackathons are available:
    - Google Maps Platform Awards (May 20 - Jul 31, 2025)
    - GenAI Hackathon by Impetus &amp;amp; AWS (May 26 - Jul 10, 2025)
    - Adobe Express Add-ons Hackathon (May 19 - Jul 15, 2025)
    - PearAI Hackathon (Jun 14 - Dec 24, 2025)
3. **Hashnode**: No direct ongoing hackathon listings found.

Summary: Currently, Devpost.com has multiple active hackathons, while Dev.to and Hashnode do not list any ongoing hackathons prominently or have no active ones at this time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fcc37ypb45827fv6c988r.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%2Fcc37ypb45827fv6c988r.png" alt="Runner H didn't find any hackathons on Dev.to"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, it did well fetching hackathons from Devpost, but it didn't find any on Dev.to, which is false because, as of writing this, there are several ongoing.&lt;/p&gt;

&lt;p&gt;Let's debug!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runner H&lt;/strong&gt; provides live preview of its web surfer agent (&lt;strong&gt;Surfer H&lt;/strong&gt;) going through its tasks (like fetching hackathon information).&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%2Fjyp562wwa8wckwh26sz5.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%2Fjyp562wwa8wckwh26sz5.png" alt="Fetching Devpost"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see it worked great on Devpost.com. It went into the specific "hackathons" page and started gathering. Why didn't it work on Dev.to?&lt;/p&gt;

&lt;p&gt;Here's the preview from &lt;strong&gt;Surfer H&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkw0iqxvbuwrglo7yexjy.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%2Fkw0iqxvbuwrglo7yexjy.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can get a glimpse of how the agent works here. It absolutely did the right thing by limiting the search on Google with &lt;code&gt;site:dev.to&lt;/code&gt; and putting in keywords like "hackathon" and "ongoing".&lt;/p&gt;

&lt;p&gt;But here's the problem: in Dev.to, hackathons are almost exclusively called challenges. So it makes sense it thought no "hackathons" were currently ongoing there. We can't expect an AI agent to know that.&lt;/p&gt;

&lt;p&gt;Let's help it out a bit!&lt;/p&gt;

&lt;p&gt;Here's the new and improved prompt where I specifically gave the agent the link to where Dev.to lists their challenges (ongoing and ended). I also gave Devpost.com a more specific URL including the filter &lt;code&gt;open&lt;/code&gt;, so hopefully it will eliminate any chance of the AI returning "ended" hackathons.&lt;/p&gt;

&lt;p&gt;This will be a great test of two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the agent distinguish ended challenges vs ongoing ones in Dev.to? Since they're all in the same page.&lt;/li&gt;
&lt;li&gt;Can the agent take into account filter parameters (see &lt;code&gt;?status[]=open&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Generate a list of currently ongoing hackathons/challenges happening at the following platforms: 
    - Devpost: [https://devpost.com/hackathons?status[]=open](https://devpost.com/hackathons?status%5B%5D=open)
    - Dev.to: https://dev.to/challenges
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are the satisfying result:&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%2Forrkf4ufgxtb5qkevbs2.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%2Forrkf4ufgxtb5qkevbs2.png" alt="Correctly fetched currently ongoing hackathons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It went into the challenges page as expected:&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%2Fvjvgeunsb1f6bt7jb2bi.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%2Fvjvgeunsb1f6bt7jb2bi.png" alt="Dev.to challenge page"&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%2F5zmrc2lto9rbxf1o2yso.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%2F5zmrc2lto9rbxf1o2yso.png" alt="Surfer H surfing Dev.to"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Takeaways
&lt;/h4&gt;

&lt;p&gt;I would say that &lt;strong&gt;Runner H&lt;/strong&gt; has passed this round. It failed to fetch the correct information on Dev.to, but it was easily fixed by giving it more context and a more complete prompt.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can't expect it to do everything! A good prompt is still needed as a starting point!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also was able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distinguish between ongoing and ended hackathons/challenges over in Dev.to&lt;/li&gt;
&lt;li&gt;It also respected the filter I had on Devpost.com&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Round 2: Using Google Sheets
&lt;/h3&gt;

&lt;p&gt;A step further than just accessing and surfing the web. Let's use one of &lt;strong&gt;Runner H&lt;/strong&gt;'s many third party connections: Google Sheets.&lt;/p&gt;

&lt;p&gt;First, you're going to want to connect your &lt;strong&gt;Runner H&lt;/strong&gt; account with Google Sheet at &lt;a href="https://runner.hcompany.ai/mcps" rel="noopener noreferrer"&gt;https://runner.hcompany.ai/mcps&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%2Fg7dbgntw7dm7zq3kk8fq.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%2Fg7dbgntw7dm7zq3kk8fq.png" alt="Connect apps page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal of this round is to test how well &lt;strong&gt;Runner H&lt;/strong&gt; integrates with Google Sheet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a file &lt;/li&gt;
&lt;li&gt;Creating that file in a specific folder&lt;/li&gt;
&lt;li&gt;Compiling gathered data into a Sheet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Including folder creation as part of the prompt unfortunately didn't work end yielded this result:&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%2Frgl746k4g7xa2o7nvd87.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%2Frgl746k4g7xa2o7nvd87.png" alt="Failed to create folder"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And I did try enabling Google Drive. Here were the results:&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%2Fi429hqa33ozg3h8pjfrn.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%2Fi429hqa33ozg3h8pjfrn.png" alt="Failed to create folder after enabling Google Drive"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I adjusted my prompt to not include it. Here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a Google Sheet named “Ongoing Hackathons” with a list of currently ongoing hackathons/challenges happening at the following platforms: 

Devpost: [https://devpost.com/hackathons?status[]=open](https://devpost.com/hackathons?status%5B%5D=open) 

Dev.to: https://dev.to/challenges

The Sheet should have the following table headers/columns:

- Name: hackathon/challenge name
- Organizer: organizer name of the hackathon (if any)
- Start date: when the hackathon/challenge will start
- Deadline: when is the deadline for submission
- Platform: either Dev.to or Devpost.com
- Link: link to the hackathon/challenge
- Prizes: the prizes to the hackathon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results were pretty good:&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%2Fjiirn9wwc8nxxnx6ouhv.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%2Fjiirn9wwc8nxxnx6ouhv.png" alt="Misplaced headers result"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However it did misplace the headers at the bottom. No worries. Let's adjust the prompt a little bit. I noticed &lt;strong&gt;Runner H&lt;/strong&gt; was able to slice its task into separate processes as you saw here:&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%2Fda2nd0x688qqxkeby62x.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%2Fda2nd0x688qqxkeby62x.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's explicitly ask it to section off its tasks using keywords like "first", "next", "finnally":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First, find all currently ongoing hackathons/challenges happening at the following platforms:

- Devpost: [https://devpost.com/hackathons?status[]=open](https://devpost.com/hackathons?status%5B%5D=open)
- Dev.to: https://dev.to/challenges

Next, for each, collect the name of the hackathon, the organizer, the relevant skills/programming language/framework/services, the start date, the deadline for submissions, the platform (either Dev.to or Devpost.com), the link to the challenge, and the prizes (if any)

Finally, add all entries into a Google Sheet named “RunnerH” in a Sheet called “Online Hackathons”.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works now:&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%2Fpj4c7ufh8qm1cv9xfo7p.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%2Fpj4c7ufh8qm1cv9xfo7p.png" alt="Generated Sheet with correct format"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Takeaways
&lt;/h4&gt;

&lt;p&gt;It mostly passed this round. It didn't seem to have the capabilities (currently) to actively create folders on your behalf.&lt;/p&gt;

&lt;p&gt;It fumbled a little when formatting the table in the Google Sheet. Then I fixed it by segmenting the prompt into separate steps.&lt;/p&gt;

&lt;p&gt;Ultimately, it did pretty well. It created the actual file, as told, then ,inside that file, created the actual Sheet with the correct name.&lt;/p&gt;

&lt;p&gt;Again, take my own experience with a grain of salt. Test it out yourself and see if you can create folders as part of the steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Round 3: File Navigation and Updating Sheet
&lt;/h3&gt;

&lt;p&gt;Let's test if &lt;strong&gt;Runner H&lt;/strong&gt; can find its way around my files and correctly update a Sheet. Let's say I forgot to add a column and wanted to append it at the end:&lt;/p&gt;

&lt;p&gt;Here's the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First, find the Google Sheet File named “RunnerH”.

Next, find the sheet in that file named “Online Hackathons”

Next, add a column at the end named “Notion Link” and fill the rows with “None”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works pretty great:&lt;br&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%2Fv18draglfdd39xr357p7.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%2Fv18draglfdd39xr357p7.png" alt="Updated Sheet"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Takeaways
&lt;/h4&gt;

&lt;p&gt;With the correct prompt, it was able to find its way to the desired file and update it accordingly. This one is a pass ✅&lt;/p&gt;
&lt;h3&gt;
  
  
  Round 4: Creating Notion Pages
&lt;/h3&gt;

&lt;p&gt;Notion pages are complex with many features. Perfect for next steps, both as a challenge for &lt;strong&gt;Runner H&lt;/strong&gt; as well as achieving my goal of organizing  ongoing hackathons.&lt;/p&gt;
&lt;h4&gt;
  
  
  Disclaimer
&lt;/h4&gt;

&lt;p&gt;I actually found out that &lt;strong&gt;Runner H&lt;/strong&gt; can't just create pages out of nowhere. So you just can't ask it something like "Create a page and write hello world". Meaning you need to do the following before doing anything with Notion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a page&lt;/li&gt;
&lt;li&gt;Give &lt;strong&gt;Runner H&lt;/strong&gt; access to that specific page&lt;/li&gt;
&lt;li&gt;Whenever you want &lt;strong&gt;Runner H&lt;/strong&gt; to use notion, reference that specific page's ID&lt;/li&gt;
&lt;/ul&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%2Fw8e7xg3ada175omntzox.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%2Fw8e7xg3ada175omntzox.png" alt=" "&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%2Fpdotmh9z5jq8bv4e52du.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%2Fpdotmh9z5jq8bv4e52du.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you don't give access to each page you want to manipulate, you'll get something like this:&lt;br&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%2Fhpnth7kfrjc6raupacc6.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%2Fhpnth7kfrjc6raupacc6.png" alt="Fail in accessing"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Let's Actually Use Notion
&lt;/h4&gt;

&lt;p&gt;Now that we have a page and access to it, we can test it out:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfx3jjqeyup5ajgn8awi.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%2Fkfx3jjqeyup5ajgn8awi.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's actually start prompting towards our goal. Here's my goal for this round:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating nested pages: So a page within a page (within one we created manually)&lt;/li&gt;
&lt;li&gt;Updating the Google Sheet with the Notion Link
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First, find the Google Sheet File named “RunnerH”.

Next, find the sheet in that file named “Online Hackathons”

Next, find the entry for &amp;lt;CHALLENGE/HACKATHON&amp;gt; and read its content.

Next, search the web for the most current information regarding that challenge/hackathon.

Next, generate 10 project ideas that fit the challenge/hackathon prompt. Each idea should have:

- General description of the project/app/software
- Recommended tech stack
- How it fits into one or more of the winning categories

Next, under the page with ID &amp;lt;NOTION PAGE ID&amp;gt; create a Notion page called “&amp;lt;the hackathon/challenge name&amp;gt; Ideas" and fill it with the generated ideas.

Finally, update the entry in Google Sheet to the new page link under the Notion Link column
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Ftx8me6td4tx3rc99qbfa.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%2Ftx8me6td4tx3rc99qbfa.png" alt=" "&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%2Fi3vwetw2vkgkzxnspgj1.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%2Fi3vwetw2vkgkzxnspgj1.png" alt=" "&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%2F0c1vhdx4jdlssvzqp8o2.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%2F0c1vhdx4jdlssvzqp8o2.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It definitely works... but the specific idea pages are not as complex as I was expecting. I tested out this prompt a bunch of times, then I got this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft1wju6tnoytghq8gcxzj.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%2Ft1wju6tnoytghq8gcxzj.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seems like we should just stick with automation for now...&lt;/p&gt;

&lt;p&gt;But it did manage to update the Google Sheet with each Notion 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%2Fg9ovutfmdsi5l4wx0fkm.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%2Fg9ovutfmdsi5l4wx0fkm.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Takeaways
&lt;/h4&gt;

&lt;p&gt;You do need to give prior access to specific Notion pages before doing anything with it. But it's not a big problem, since once you create one you can make any nested structure with your pages.&lt;/p&gt;

&lt;p&gt;I think &lt;strong&gt;Runner H&lt;/strong&gt; should definitely make updating page permissions a bit easier though, since the way I have to do it in time of writing is to disable the Notion connection and re-give it the correct page permissions.&lt;/p&gt;

&lt;p&gt;It also updated the Google Sheet correctly.&lt;/p&gt;

&lt;p&gt;A problem was how it struggled with creative content creation (i.e. generating project ideas). Maybe I just prompted poorly. It even made me do it outside the session:&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%2F9dt2tps9c3wdqn4wktwk.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%2F9dt2tps9c3wdqn4wktwk.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I'd say overall it's a pass with some adjustments.&lt;/p&gt;
&lt;h3&gt;
  
  
  Round 5: Putting It All Together
&lt;/h3&gt;

&lt;p&gt;Since it seems to struggle a bit with creative tasks, let's keep it within  automation. I also wanted to skip the Google Sheet, as it was redundant. I only did that as part of the previous rounds' requirements. &lt;/p&gt;

&lt;p&gt;Here's my goal for this round: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a notion page under in the parent page with the provided ID&lt;/li&gt;
&lt;li&gt;Fill that page with a weekly digest of the current ongoing hackathons -- newspaper style&lt;/li&gt;
&lt;li&gt;If page  already has content, append the new page instead of replacing it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the final prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are my Weekly Hackathon Digest Agent.

Fetch at least 10 ongoing or upcoming hackathons from:

- Devpost: [https://devpost.com/hackathons?status[]=open](https://devpost.com/hackathons?status%5B%5D=open)
- Dev.to: https://dev.to/challenges

    If there are fewer than 10, include as many as possible.


For each hackathon, collect:

- Name
- Organizer (if available)
- Prompt or challenge summary
- Relevant skills, programming languages, frameworks, services
- Start date and submission deadline
- Platform (Devpost or Dev.to)
- Direct link
- Prizes (if any)
- Number of days left until the deadline

If the deadline is within the next 7 days, add a label like: DEADLINE APPROACHING

Create a new Notion page under the parent page with ID &amp;lt;NOTION PAGE ID&amp;gt;

Name the page: Weekly Hackathon Digest – Week of YYYY-MM-DD

If a page with the same name already exists, add (1), (2), etc. at the end, like Windows file naming.

Inside the page, create properly structured Notion blocks:

- Add a heading block at the top: Weekly Hackathon Digest
- Add today’s date as a text block
- For each hackathon, create:
    - A heading block with the hackathon name
    - Paragraph blocks listing the organizer, prompt, relevant skills, start date and deadline, platform, prizes
    - Add a rich text block with clickable text like: “Link to challenge” using the direct link as the URL
    - If the deadline is within 7 days, add a paragraph block saying: DEADLINE APPROACHING
- Use divider blocks or spacing blocks to separate hackathons clearly
- At the end, add paragraph blocks with:
    - Total number of hackathons listed
    - Number of hackathons with deadlines in the next 7 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the resulting Notion 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%2F8zxutrgaz2h20g7hsqea.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%2F8zxutrgaz2h20g7hsqea.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty great. It used correct formatting (heading, bold text, links, etc.)&lt;/p&gt;

&lt;p&gt;I tried using the prompt again to make sure it can append to the Notion page if there was an existing page (the result of the previous prompt). And it works:&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%2Frymbd8dve6bfasssh3fh.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%2Frymbd8dve6bfasssh3fh.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Takeaways
&lt;/h4&gt;

&lt;p&gt;After simplifying the prompts and skipping Google Sheet. I actually achieved my final goal of organizing a list of the currently ongoing hackathons/challenges in a weekly digest-style Notion page.&lt;/p&gt;

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

&lt;p&gt;Again, I'd like to reiterate that the results of my experiment are based on my experience and my skill level in prompting. So don't take it as concrete documentation of what Runner H is capable of. Run your own experiments. You can use this article as more of a guideline.&lt;/p&gt;

&lt;p&gt;So with that said, here were the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Round #1: Fetching Current Data -&amp;gt; ✅✅✅✅✅
I started with a very vague prompt to fetch ongoing hackathons happening at Dev.to and Devpost.com. It mistakenly thought there were no hackathons currently open over on Dev.to, even though it was not the case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I narrowed down the problem to the fact that Dev.to calls hackathons challenges. This was understandable. A good prompt is needed as much as a capable agent is. So I adjusted the prompt to be more specific by including direct links.&lt;/p&gt;

&lt;p&gt;The results were great. And &lt;strong&gt;Runner H&lt;/strong&gt; was even capable of distinguishing between ongoing and finished challenges over in Dev.to, even though they are in the same page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Round #2: Using Google Sheets -&amp;gt; ✅✅✅☐☐
In this round I messed around with folder creation. This is where I encountered my first assumed limitation.&lt;/li&gt;
&lt;/ul&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%2Fumvxh930s4ot2dcruny1.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%2Fumvxh930s4ot2dcruny1.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again, I encourage you to try it yourself to test if this was an actual limitation. Even if it is, it not too bad. The goal of creating folders in the first place is for organization. But as you'll see on the next round, &lt;strong&gt;Runner H&lt;/strong&gt; is pretty good at file navigation. So in the context of automation, it doesn't impact much.&lt;/p&gt;

&lt;p&gt;Creating Google Sheets yielded much better results. There were a couple of mistakes. Most commonly, sometimes table headers are generated in the wrong place, namely at the bottom of the actual rows.&lt;/p&gt;

&lt;p&gt;This is rare enough that it didn't really affect my process that much, but if anyone has found a way to consistently generate table headers in Sheet correctly, let me know.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Round #3: File Navigation and Updating Sheet -&amp;gt; ✅✅✅✅✅
I was super pleased with this round. I had a Google Sheet file, and inside that file I had a specifically named Sheet. I wanted &lt;strong&gt;Runner H&lt;/strong&gt; to find that specific sheet in a specific file... and then update it.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First, find the Google Sheet File named “RunnerH”.

Next, find the sheet in that file named “Online Hackathons”

Next, add a column at the end named “Notion Link” and fill the rows with “None”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is an example. The results were great and it updated correctly. So full marks in this round.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Round #4: Creating Notion Pages -&amp;gt; ✅✅✅✅☐
Here are some notes about &lt;strong&gt;Runner H&lt;/strong&gt;'s Notion integration:
&lt;strong&gt;Runner H&lt;/strong&gt; cannot arbitrarily create pages. Meaning you can't just ask it to create a page out of nowhere. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a limitation on &lt;strong&gt;Runner H&lt;/strong&gt; rather in how Notion handles integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runner H&lt;/strong&gt; can only interact with pages you've given it permission to. So if you prompt that writes into Notion, you'd need to create a base page and give &lt;strong&gt;Runner H&lt;/strong&gt; specific action to that page.&lt;/p&gt;

&lt;p&gt;Then in the prompt, you can reference it with the page ID.&lt;/p&gt;

&lt;p&gt;In interacting with Notion itself, &lt;strong&gt;Runner H&lt;/strong&gt; does a great job. It could create pages within pages within pages (as long as you have permission for the base page).&lt;/p&gt;

&lt;p&gt;If I had to nitpick, though, it's the way &lt;strong&gt;Runner H&lt;/strong&gt; handles updating page permissions. I haven't found a good way to grant access to more pages. I had to constantly disable Notion and re-enable the integration/connection and select the new page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Round #5: Putting It All Together -&amp;gt; ✅✅✅✅✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Putting it all together into I final prompt gave me very satisfying results. Here's the workflow: I run the prompt, it &lt;strong&gt;APPENDS&lt;/strong&gt; a new page into my base Notion page, and finally fills it with information about currently ongoing hackathons.&lt;/p&gt;

&lt;p&gt;I'm impressed that it appends to existing content so effectively:&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%2Fb228lirt8qzzt8s2yidz.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%2Fb228lirt8qzzt8s2yidz.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Runner H
&lt;/h2&gt;

&lt;p&gt;Here were the integrations I tested out during my time exploring Runner H:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notion: for creating and writing into pages&lt;/li&gt;
&lt;li&gt;Sheets: compiling data into tables&lt;/li&gt;
&lt;li&gt;Surfer H: used a lot in gathering current data&lt;/li&gt;
&lt;li&gt;Google Docs: Works pretty much the same as Notion pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Case &amp;amp; Impact
&lt;/h2&gt;

&lt;p&gt;Like I said many times, use this article as a general glimpse of the capabilities and current limitations of Runner H. And take this whole thing with a grain of salt. Hopefully this article inspires you to try Runner H on your own.&lt;/p&gt;

&lt;p&gt;The limitations I described above might be circumvented by better prompt engineering.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>runnerhchallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Blok Tales: A Visual Novel Made with Storyblok</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Thu, 26 Jun 2025 14:52:05 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/blok-tales-a-visual-novel-made-with-storyblok-1d26</link>
      <guid>https://forem.com/ansellmaximilian/blok-tales-a-visual-novel-made-with-storyblok-1d26</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/storyblok"&gt;Storyblok Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Inspired by &lt;a href="https://www.renpy.org/" rel="noopener noreferrer"&gt;Ren'Py&lt;/a&gt;, a visual novel engine for Python developers, I set out to bring this idea over to the web.&lt;/p&gt;

&lt;p&gt;I call it &lt;strong&gt;Blok Tales&lt;/strong&gt;. The general idea was to make making visual novels on the web very easy but still customizable should the need arise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.storyblok.com/" rel="noopener noreferrer"&gt;Storyblok&lt;/a&gt; was perfect for this. Storyblok allows me (and other users) to create visual novel scenes that can connect to one another and even branch out based on choices.&lt;/p&gt;

&lt;p&gt;Storyblok allows me to ADD, REMOVE, and EDIT content from the comfort of their dashboard AND visual editor. While the headless nature of Storyblok itself allows me to modify the looks through the React application I've created.&lt;/p&gt;

&lt;p&gt;If I need to add a scene or a dialog, I can go over to my dashboard and do it there. If I ever decide to create a whole different theme for my visual novel, I can do that too. I'll go over to my React app and change a few classes, shift a few components around, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Story
&lt;/h3&gt;

&lt;p&gt;I want to give a very brief summary of the story I created for demo purposes. It's about a Dev in Webtopia, confronting "The Monolith" a bloated monolithic traditional CMS, which then leads to the discovery of "Storyblock" a lighter, headless alternative.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/krOX8d6n5Jw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Visual Novel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can try out the visual novel live &lt;a href="http://bloktales.netlify.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storyblok Space:&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Here is a link to duplicate my space: &lt;a href="https://app.storyblok.com/#!/build/342867" rel="noopener noreferrer"&gt;https://app.storyblok.com/#!/build/342867&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;OF NOTE: The link doesn't seem to be working for other users. I, the owner, can use this; but I can't figure out how to make this link public. If anyone knows, please let me know in the comments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Code Repository:&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;Here's a link to my React app that renders the visual novel content from Storyblok, in case you want to create your own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AnsellMaximilian/blok-tales" rel="noopener noreferrer"&gt;https://github.com/AnsellMaximilian/blok-tales&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd like to create your own design with themes of your choice, you can clone the repo and connect to your own space. Look at the README file on the repo for instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo Video or Screenshots&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are some snippets of scenes I created with Blok Tales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Main Menu&lt;br&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%2Fyagv6q1xi5qg5qaxu3h4.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%2Fyagv6q1xi5qg5qaxu3h4.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Narration&lt;br&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%2Fe7jcz1w7mm15d8kl6nmg.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%2Fe7jcz1w7mm15d8kl6nmg.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dialog with Character&lt;br&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%2Fwnbup9qpgrrxlnhficw9.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%2Fwnbup9qpgrrxlnhficw9.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choices&lt;br&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%2Fsjose5okenx1kdqe1bnw.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%2Fsjose5okenx1kdqe1bnw.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;Here is my tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React: used to map stories and custom blocks into reusable components&lt;/li&gt;
&lt;li&gt;TailwindCSS: used to style components&lt;/li&gt;
&lt;li&gt;React Router: used for easily mapping URL paths to story slugs&lt;/li&gt;
&lt;li&gt;Framer Motion: used for smooth transitions between dialogs and narrations for each scene&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I Used Storyblok
&lt;/h3&gt;

&lt;p&gt;At its core, Blok Tales is smooth collaboration between my React components and my Storyblok's space, which I've configured specifically to support scene creations.&lt;/p&gt;

&lt;p&gt;You can clone my space, or create your own using this schema:&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%2Fupb7le3lixxmkbding5j.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%2Fupb7le3lixxmkbding5j.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Legend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The container represents a scene, which is a content type&lt;/li&gt;
&lt;li&gt;Blue icons are assets in my Storyblok space&lt;/li&gt;
&lt;li&gt;Each white box is a &lt;a href="https://www.storyblok.com/docs/concepts/blocks" rel="noopener noreferrer"&gt;block&lt;/a&gt;-type field within a content type (story or another custom block)&lt;/li&gt;
&lt;li&gt;Each green block is a custom component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, to give an example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have a scene (story) called "prologue"&lt;/li&gt;
&lt;li&gt;The scene has the following:

&lt;ol&gt;
&lt;li&gt;Background: is an asset (image)&lt;/li&gt;
&lt;li&gt;Music: is also an asset (music)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Content&lt;/code&gt;: is a &lt;code&gt;block&lt;/code&gt; type, meaning it will be comprised of an array of other blocks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Choices&lt;/code&gt;: is also a &lt;code&gt;block&lt;/code&gt;, containing specifically the &lt;code&gt;choice&lt;/code&gt; custom block&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Content&lt;/code&gt; will have children of the following type: &lt;code&gt;Narration&lt;/code&gt; and &lt;code&gt;Dialog&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Narration&lt;/code&gt; is just a custom block type containing one single block: text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dialog&lt;/code&gt; is a little more complex. It's also a custom block; it also has text. But it has another thing: &lt;code&gt;Character&lt;/code&gt;-- yet another custom block.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Character&lt;/code&gt; itself... is another custom block, which has the &lt;code&gt;image&lt;/code&gt; field for character sprites and name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see you can create pretty &lt;strong&gt;complex&lt;/strong&gt; schemes using Storyblok.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storyblok Features Used
&lt;/h3&gt;

&lt;p&gt;I want to use this section to highlight specific features of Storyblok I utilized to realize my vision to make a web based visual novel. And to do that I'll go through what creating a scene looks like. We'll use an actual &lt;code&gt;scene&lt;/code&gt; I have in my space; it's called "Confronting The Monolith":&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating a new content type
&lt;/h4&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%2F82wsq0kx1iwn9jdm2yp8.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%2F82wsq0kx1iwn9jdm2yp8.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Storyblok, you generate content by making stories, this usually are pages. But you can go further and generate different types of stories. This way you can distinguish between them in terms of how you render them.&lt;/p&gt;

&lt;p&gt;For example, I render &lt;code&gt;scene&lt;/code&gt;-type stories differently than regular pages. This gives you a huge flexibility and removes any restriction in what types of pages you want to display.&lt;/p&gt;

&lt;h4&gt;
  
  
  Whitelisting types of blocks to add
&lt;/h4&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%2Fmjiyl783ipv8lpnuowls.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%2Fmjiyl783ipv8lpnuowls.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above image shows me creating "content" within scenes. I want to mention a really cool feature that Storyblok provides, which is whitelisting OR limiting what types of custom blocks can be added within a block-type field.&lt;/p&gt;

&lt;p&gt;I found this really useful both for establishing tight control over your content as well as to limit the choices when adding blocks so you don't get overwhelmed by block types you KNOW you won't need.&lt;/p&gt;

&lt;p&gt;Anyways, for my case, I've limited the &lt;code&gt;content&lt;/code&gt; field of my &lt;code&gt;scene&lt;/code&gt;s to only include &lt;code&gt;narration&lt;/code&gt; and &lt;code&gt;dialog&lt;/code&gt;, which are basically just text boxes furthering the story forward. I can't add any other types of blocks there; this way I can be sure there won't be any expected blocks in there that will show up.&lt;/p&gt;

&lt;h4&gt;
  
  
  Ordering blocks
&lt;/h4&gt;

&lt;p&gt;As I've said, &lt;code&gt;content&lt;/code&gt; consists of narration and dialog. You hit right click, it moves the story forward with either a text of narration or a dialog from a character.&lt;/p&gt;

&lt;p&gt;I want to mention how useful it was that order of these blocks MATTER. This way, if I ever need to change the order around, I can do that; and it will be reflected in the frontend immediately.&lt;/p&gt;

&lt;p&gt;Or if I ever need to add a narration in the middle, I can do that.&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating presets for characters
&lt;/h4&gt;

&lt;p&gt;As described in the schema, each dialog consists of a character and a text of what the character is saying.&lt;/p&gt;

&lt;p&gt;Character is another custom block type. &lt;/p&gt;

&lt;p&gt;Another cool feature I want to mention is the ability to create presets. Obviously filling character details for each dialog will be repetitive and slow. But with Storyblok I can actually save a specific instance of a custom block with prefilled fields.&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%2Flu354eh1zncegtywuxu1.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%2Flu354eh1zncegtywuxu1.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here I've created all the presets for my characters. This way I can avoid filling the same fields with the same values each time I want my characters to talk more than once.&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating scene references
&lt;/h4&gt;

&lt;p&gt;At the end of each scene I have choices. These are custom blocks that have the following field: &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;next_scene&lt;/code&gt;. Label is a regular text field for displaying the choice. But &lt;code&gt;next_scene&lt;/code&gt; actually is a reference to another scene.&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%2F3b06xw9yxzj2qofz3a3x.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%2F3b06xw9yxzj2qofz3a3x.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to mention that Storyblok returns only the UUID for each scene by default. To have any meaningful data returned from references you have to establish relations. Read the docs &lt;a href="https://www.storyblok.com/docs/concepts/references#resolving-relations-in-api-requests" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks to the reference block type, I can actually create branching choices and narrative decisions within Blok Tales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learnings and Takeaways
&lt;/h2&gt;

&lt;p&gt;I had a ton of fun learning about how to use Storyblok to my advantage to create this visual novel engine.&lt;/p&gt;

&lt;p&gt;It honestly surprised me how many features directly supported my vision and how I wanted to implement the visual novel engine.&lt;/p&gt;

&lt;p&gt;I had some trouble with asset caching, but other than that everything went dandy!&lt;/p&gt;

&lt;h3&gt;
  
  
  Image editor
&lt;/h3&gt;

&lt;p&gt;I want to sneak this one in real quick. The image editor is SUPER useful. Sometimes I create the character sprite backwards, and there it was. The "flip horizontally" feature on Storyblok. It saved me a ton of time.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>storyblokchallenge</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Making Postr: Email-First App with Postmark's Inbound Parsing</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Mon, 09 Jun 2025 01:34:01 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/making-postr-email-first-app-with-postmarks-inbound-parsing-3m9p</link>
      <guid>https://forem.com/ansellmaximilian/making-postr-email-first-app-with-postmarks-inbound-parsing-3m9p</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;
  
  
  Quick Video Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/__BLWuKxhPk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

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

&lt;p&gt;Postr is a powerful messaging / content creation app built around the idea of email-first interaction, powered by Postmark's very own inbound email parsing feature. Instead of building a third party app where users have to sign up, learn a new UI, and understand an app -- Postr provides an alternative: most of your time using it will be done through email, the most universally understood content/messaging medium on the web.&lt;/p&gt;

&lt;p&gt;Users can initiate threads, gather responses, and finally generate beautifully themed AI-powered email responses — all without needing other participants to ever visit the app itself.&lt;/p&gt;

&lt;p&gt;This core idea will be useful if you need to collaborate with other people who are not as tech savvy as you or even people who aren't very easy to convince to take their time to learn a new app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things Postr Can Create
&lt;/h3&gt;

&lt;p&gt;Postcards are the center of Postr, and a postcard is some form of content generated by a collection of responses by participants who will not be using your app at all. A specific scenario will be explained later to further explain.&lt;/p&gt;

&lt;p&gt;Here are the three main forms of content each postcard can take:&lt;/p&gt;

&lt;h4&gt;
  
  
  Polls/Survey/Group Decision
&lt;/h4&gt;

&lt;p&gt;Prompt participants to decide something as a group. Collect and summarize the responses to arrive at a shared outcome. Great for group planning (e.g., choosing a restaurant, date, or gift).&lt;/p&gt;

&lt;h4&gt;
  
  
  Tributes/Sentiments
&lt;/h4&gt;

&lt;p&gt;A collaborative tribute where each participant contributes a heartfelt message. Ideal for birthdays, weddings, farewells, or celebrating someone's achievement.&lt;/p&gt;

&lt;h4&gt;
  
  
  Recap/Memories
&lt;/h4&gt;

&lt;p&gt;Summarizes what everyone shared in response to a prompt — like reflections after a group trip, a retrospective, or how people felt about a recent event or experience.&lt;/p&gt;

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

&lt;p&gt;Try out the app yourself: &lt;a href="https://postmark-postr.vercel.app/" rel="noopener noreferrer"&gt;https://postmark-postr.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a general workflow of how you would use this app. A more specific/realistic scenario will be mentioned at the end to help you picture it more clearly:&lt;/p&gt;

&lt;h3&gt;
  
  
  Login/Register
&lt;/h3&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%2Fxz2ikj780fqc2mvktmmf.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%2Fxz2ikj780fqc2mvktmmf.png" alt="Login View"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Postcard
&lt;/h3&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%2Fqgfkjduu5ghutfwkn8ax.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%2Fqgfkjduu5ghutfwkn8ax.png" alt="Home page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start by creating a Postcard entry in Postr. You know your way around new apps, so your group is relying on you to spearhead this.&lt;/p&gt;

&lt;p&gt;Fill all the necessary fields such as mood, theme colors, etc. This will affect the final result of the content.&lt;/p&gt;

&lt;p&gt;You can also add notes to influence the end result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Postcard: Waiting for Initial Email
&lt;/h3&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%2Fve4tfo8jvbfkl40jkiqd.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%2Fve4tfo8jvbfkl40jkiqd.png" alt="Postcard waiting for initial email"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you click on your postcard on the left, you can view it in detail on the right.&lt;/p&gt;

&lt;p&gt;Right now it's in its starting state, waiting for that initial email-- that email where you prompt your participants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send Initial Email
&lt;/h3&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%2Fvrq2z9qmqyy1xseu1qzh.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%2Fvrq2z9qmqyy1xseu1qzh.png" alt="Postcard instructions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can compose the email manually, just make sure you start your subject with &lt;code&gt;[postcard id]&lt;/code&gt; followed by your actual subject and add at least 2 CCs.&lt;/p&gt;

&lt;p&gt;But a more convenient way is to just click the "Compose in Gmail" button, which will lead to Gmail's compose view with some prefilled attributes for your convenience.&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%2Fvdyvyepci1m200dv7l89.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%2Fvdyvyepci1m200dv7l89.png" alt="Gmail compose view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is Gmail's compose view. You can take advantage of its features like recipient autocomplete/suggestion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Postcard: Waiting for Replies
&lt;/h3&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%2Fbh6ov06tnsygbcuc83ws.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%2Fbh6ov06tnsygbcuc83ws.png" alt="Postcard waiting for replies "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you've sent the initial email, your postcard will enter the next stage: waiting for replies.&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%2Fc5d529uyodq8pudrf25d.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%2Fc5d529uyodq8pudrf25d.png" alt="Postcard waiting for replies detail"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, it lists all the CCs that need to reply for you to send the final content email.&lt;/p&gt;

&lt;p&gt;Now you just have to wait for your participants to respond.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: Make sure to let your participants know to "Reply All" to make sure the response gets recorded&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Click Send Email Button
&lt;/h3&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%2Fbogk9qk6trr33jcm666v.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%2Fbogk9qk6trr33jcm666v.PNG" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once all participants (CCs) have answered, a button appears. Click the "Send Postcard Email" button. This will take quite a bit, as this is the part where content will be generated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Postcard: Content Email Generated
&lt;/h3&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%2Fl0grdy7mok9pmtnri4n6.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%2Fl0grdy7mok9pmtnri4n6.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the email has been generated, you can view it directly in your postcard. But more importantly...&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Email Sent as Part of the Original Thread
&lt;/h3&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%2F9m1zr25ve3ycr1zzjut7.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%2F9m1zr25ve3ycr1zzjut7.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the final generated postcard content email has been sent to you and all the participants.&lt;/p&gt;

&lt;p&gt;Even better, it has been sent as part of the original thread. So all history of your conversation can be seen in one place.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/AnsellMaximilian/poster" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Postr was built in Next.js with Tailwind CSS, TypeScript, Appwrite, AI email generation, and of course &lt;strong&gt;Postmark&lt;/strong&gt;. But the core magic lies in how I integrated Postmark’s inbound email parsing to make email not just a feature, but the central interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Inbound Email Parsing as the Core
&lt;/h3&gt;

&lt;p&gt;Postmark allows you to set up an inbound email address (like &lt;a href="mailto:anything@inbound.postr.app"&gt;anything@inbound.postr.app&lt;/a&gt;). Any email sent to that address gets parsed into a clean JSON payload that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sender, recipient, subject&lt;/li&gt;
&lt;li&gt;Cleaned HTML and plaintext body&lt;/li&gt;
&lt;li&gt;Attachments&lt;/li&gt;
&lt;li&gt;Headers (e.g. In-Reply-To, References, Message-ID)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets Postr:&lt;br&gt;
-Create new postcards based on the first inbound email&lt;br&gt;
-Track threads and replies by matching headers&lt;br&gt;
-Update the postcard in real-time based on replies&lt;br&gt;
-Generate a final AI-crafted email that is sent to all participants&lt;/p&gt;
&lt;h3&gt;
  
  
  📨 Sending Final Content Email Postcard with Postmark's API
&lt;/h3&gt;

&lt;p&gt;Of course, I needed to send the final generated content postcard to all the users. Postmark  makes this easy with the &lt;code&gt;/email&lt;/code&gt; endpoint. You can learn more about it &lt;a href="https://postmarkapp.com/developer/user-guide/send-email-with-api" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I want to keep everything centralized and connected. All the replies will already be threaded automatically, but  I needed to make sure that the final email also gets sent as part of the original thread. &lt;/p&gt;

&lt;p&gt;For replies, users just need to click “Reply All” in their email client, it will handle the rest. This means only one participant ever needs to open Postr to contribute.&lt;/p&gt;

&lt;p&gt;ButI still need to think about the final email and how to connect it to the original thread. Here's how I did it:&lt;/p&gt;
&lt;h3&gt;
  
  
  🧵 Threading with Email Headers
&lt;/h3&gt;

&lt;p&gt;One of the biggest features I leaned on is threading. Postmark gives you access to the &lt;code&gt;Message-ID&lt;/code&gt; of parsed emails, and from there I use the &lt;code&gt;In-Reply-To&lt;/code&gt; and &lt;code&gt;References&lt;/code&gt; headers when sending the final reply.&lt;/p&gt;

&lt;p&gt;But that's not everything you need for including an email in a thread. You also need to set the subject in this format &lt;code&gt;Re: &amp;lt;original subject&lt;/code&gt;. Luckily Postmark's inbound parsing also includes the subject (obviously).&lt;/p&gt;

&lt;p&gt;So what you send to Postmark's outbound API should look like this:&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;--location&lt;/span&gt; &lt;span class="s1"&gt;'https://api.postmarkapp.com/email'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'X-Postmark-Server-Token: &amp;lt;YOUR_SERVER_TOKEN&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-raw&lt;/span&gt; &lt;span class="s1"&gt;'{
  "From": "your@email.com",
  "To": "receiver@email.com",
  "Subject": "Re: &amp;lt;ORIGINAL_SUBJECT&amp;gt;,
  "TextBody": "Text body content",
  "HtmlBody": "&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;strong&amp;gt;HTML BODY&amp;lt;/strong&amp;gt;.&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;",
  "MessageStream": "outbound",
  "Headers": [
    {
      "Name": "In-Reply-To",
      "Value": "&amp;lt;ORIGINAL_EMAIL_MESSAGE_ID&amp;gt;"
    },
    {
      "Name": "References",
      "Value": "&amp;lt;ORIGINAL_EMAIL_MESSAGE_ID&amp;gt;"
    }
  ]
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: &lt;code&gt;&amp;lt;ORIGINAL_EMAIL_MESSAGE_ID&amp;gt;&lt;/code&gt; should come from the original email's &lt;code&gt;Headers&lt;/code&gt; property (&lt;code&gt;Message-ID&lt;/code&gt;), not &lt;code&gt;MessageID&lt;/code&gt; in the root level. You can get this when parsing the original email&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For more info on how to thread with Postmark, here's a &lt;a href="https://postmarkapp.com/support/article/1276-threading-unthreading-messages" rel="noopener noreferrer"&gt;blog by Postmark&lt;/a&gt;. This is what I used and it was very useful in understanding threading.&lt;/p&gt;

&lt;h3&gt;
  
  
  🏖️ EXAMPLE: The Family Trip Scenario
&lt;/h3&gt;

&lt;p&gt;Let’s say you want to plan a family trip. You use Postr to create a new postcard of type "Group Decision," and add a note to theme the postcard around "Love". Once created, Postr generates an initial email draft you can send via Gmail to your family, with two or more CCs.&lt;/p&gt;

&lt;p&gt;The family members receive that email in their regular inboxes and simply reply with their preferences.&lt;/p&gt;

&lt;p&gt;One cousin says: "Let’s go to Disneyland!"&lt;br&gt;
Another replies: "SeaWorld could work, but honestly, Disneyland sounds fun."&lt;/p&gt;

&lt;p&gt;As soon as all CCs reply, Postr updates the postcard and enables you to trigger the final postcard creation — a beautiful, expressive, HTML-formatted email generated by AI based on the group’s responses.&lt;/p&gt;

&lt;p&gt;It gets sent back to all participants in the same thread, thanks to the headers and threading. From start to finish, the entire flow happened over email — no app downloads, no learning curve.&lt;/p&gt;

&lt;p&gt;Here's the generated content:&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%2Fx613gbysc8h84m2dxchf.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%2Fx613gbysc8h84m2dxchf.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Postr was built to demonstrate how inbound email can be a first-class user interface. Postmark made that not just possible but joyful to work with. It's fast, reliable, and delightfully simple to use. I can't wait to keep building with it.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>postmarkchallenge</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>PermiShout: Recreating Twitter with Access Control using Permit.io</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Mon, 05 May 2025 02:56:41 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/permishout-recreating-twitter-with-access-control-using-permitio-16j0</link>
      <guid>https://forem.com/ansellmaximilian/permishout-recreating-twitter-with-access-control-using-permitio-16j0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/permit_io"&gt;Permit.io Authorization Challenge&lt;/a&gt;: Permissions Redefined&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you want to immediately get setup. Follow the instructions at &lt;a href="https://github.com/AnsellMaximilian/permishout" rel="noopener noreferrer"&gt;https://github.com/AnsellMaximilian/permishout&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;For this challenge, I decided to recreate Twitter/X, complete with access control. It's called PermiShout. Where a &lt;code&gt;Shout&lt;/code&gt; is like a &lt;code&gt;Tweet&lt;/code&gt; or post. I thought this would be perfect to show off &lt;a href="https://www.permit.io/" rel="noopener noreferrer"&gt;Permit.io's&lt;/a&gt; base capabilities.&lt;/p&gt;

&lt;p&gt;Here's the app workflow:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Login/Signup
&lt;/h3&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%2Fzr0extb61coo39a0zk5a.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%2Fzr0extb61coo39a0zk5a.png" alt="Clerk login"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Complete Profile
&lt;/h3&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%2Fx6r0ndsc6ko1tasdpsk7.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%2Fx6r0ndsc6ko1tasdpsk7.png" alt="Profile completion"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Shout
&lt;/h3&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%2Fcnyzhqaahl11hl625ms3.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%2Fcnyzhqaahl11hl625ms3.png" alt=" "&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%2F9k2mpawuf91ulik5r74h.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%2F9k2mpawuf91ulik5r74h.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Look at Profile
&lt;/h3&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%2Fgtiqwjw58ur3senykfqr.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%2Fgtiqwjw58ur3senykfqr.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow/Unfollow
&lt;/h3&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%2Ftp3fzkg1tgvyi0stnx67.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%2Ftp3fzkg1tgvyi0stnx67.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reply to and Delete Shouts
&lt;/h3&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%2F0x4khea5hnxx71p52816.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%2F0x4khea5hnxx71p52816.png" alt=" "&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%2Fl6jzrwo8v1lyg0xcfk68.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%2Fl6jzrwo8v1lyg0xcfk68.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see some shouts are different than others. Some have a delete button -- that's because Permit.io has determined that the current user has the authorization to do that.&lt;/p&gt;

&lt;p&gt;Some reply buttons are disabled and some are not. That's also Permit.io doing the work. Here's some conditions that has been set up  for a user to be able to reply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the shout reply mode is set to "everyone" then any one can reply&lt;/li&gt;
&lt;li&gt;If the shout reply mode has been set to "verified accounts only" then only "admins" can reply&lt;/li&gt;
&lt;li&gt;If the shout reply mode has been set to "people mentioned" then only people mentioned will be able to reply&lt;/li&gt;
&lt;li&gt;Everyone the user is following can reply&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/cMGdF1q73Yo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Repo
&lt;/h2&gt;

&lt;p&gt;Check out the repo &lt;a href="https://github.com/AnsellMaximilian/permishout" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Journey
&lt;/h2&gt;

&lt;p&gt;I have to say it wasn't easy learning all the necessary topics about Permit.io. It is definitely robust and full of features you would expect from an access control library.&lt;/p&gt;

&lt;p&gt;But will say that one of the most frustrating but intriguing part was learning about integrating Permit.io on the frontend using the &lt;a href="https://github.com/permitio/permit-fe-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;permit-fe-sdk&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I finally got it to work, and it's definitely one of my favorite parts of Permit.io. And I got a custom reusable context provider out of this to use for future projects using Permit.io. &lt;a href="https://github.com/AnsellMaximilian/permishout/blob/main/context/permission/AbilityContextProvider.tsx" rel="noopener noreferrer"&gt;Custom Permission Context Provider&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Overall, this was my first time using a third party access control service and from what I've seen it's very robust and complete.&lt;/p&gt;

&lt;p&gt;In my head I keep comparing it with the ease of access control/authorization in Laravel. Which is great! It's one of the main reasons I love using Laravel even though NextJs is my favorite framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Permit.io for Authorization
&lt;/h2&gt;

&lt;p&gt;The way I'm going to structure this section is by going through the development as if I'm developing Permishout from scratch. Letting you know  what decisions need to be made while developing with Permit.io. This way, you'll be able to apply my journey into your own while developing your own apps with Permit.io.&lt;/p&gt;

&lt;p&gt;Here are a bunch of things that I want to highlight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Planning and Setting Up&lt;/li&gt;
&lt;li&gt;Syncing Users&lt;/li&gt;
&lt;li&gt;Doing checks in the backend&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;permit-fe-sdk&lt;/code&gt; for easily displaying things based on access control&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Planning and Setting Up
&lt;/h3&gt;

&lt;h4&gt;
  
  
  User Stories
&lt;/h4&gt;

&lt;p&gt;So first I want to identify user stories that I want for this app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User authentication.&lt;/li&gt;
&lt;li&gt;Authenticated users have &lt;code&gt;profile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All authenticated users will be able to &lt;code&gt;create&lt;/code&gt; any &lt;code&gt;shout&lt;/code&gt; and also view it&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;shouter&lt;/code&gt; (poster/author) of a &lt;code&gt;shout&lt;/code&gt; will be able to &lt;code&gt;delete&lt;/code&gt; and &lt;code&gt;reply&lt;/code&gt; to it.&lt;/li&gt;
&lt;li&gt;Users with the role &lt;code&gt;admin&lt;/code&gt; will be able to &lt;code&gt;delete&lt;/code&gt; any &lt;code&gt;shout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Authenticated users can follow/unfollow each other.&lt;/li&gt;
&lt;li&gt;In Twitter/X you can set which users will be able to reply to your Tweet/Post. I want to emulate this feature, as it will show off a lot of the capabilities in Permit.io. Here's how Twitter/X handles it:

&lt;ol&gt;
&lt;li&gt;Everyone: everyone can reply to a &lt;code&gt;shout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;People mentioned: only people &lt;code&gt;mentioned&lt;/code&gt; in a &lt;code&gt;shout&lt;/code&gt; can &lt;code&gt;reply&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;People you follow: only people you follow can &lt;code&gt;reply&lt;/code&gt;. However to demonstrate "role derivations", I'm going to make it so people you follow can always reply to any of your &lt;code&gt;shout&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;Verified users only: For PermiShout, it's going to be people with &lt;code&gt;admin&lt;/code&gt; roles.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Identifying Components
&lt;/h4&gt;

&lt;p&gt;Now that we have our user stories, we can start identifying Permit.io components we are going to need.&lt;/p&gt;

&lt;p&gt;Here's what I got:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Roles:&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;admin&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resources:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;profile&lt;/code&gt;&lt;br&gt;
This resource will just be an anchor point of many of the functionalities I'm going to implement. It's not going to hold any attributes, but it will  help me identify followers/following as well as role derivations. This will be explained later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;shout&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resource instance roles:&lt;br&gt;
Just a little explanation on what instance roles are: Admin would be a Top  Level Role, where authorizations on it will be applied to every instance of a resource. So if an Admin has &lt;code&gt;delete&lt;/code&gt; access to a &lt;code&gt;shout&lt;/code&gt;, it will have that on every one.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An instance role, however, only applies to a single instance of a resource type. Here's how Permit.io structures it: &lt;code&gt;resource:resource_key#role&lt;/code&gt;, meaning actions available to the role owner will only apply to resource with the key &lt;code&gt;resource_key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here are the roles I have identified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;profile#owner&lt;/code&gt;
Just the owner of a profile. Just like Twitter having you complete some data before you can Tweet.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;profile#follower&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;profile#followed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shout#shouter&lt;/code&gt;: can &lt;code&gt;reply&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; a shout&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shout#replier&lt;/code&gt;: can &lt;code&gt;reply&lt;/code&gt; to a &lt;code&gt;shout&lt;/code&gt;
This will be an important role. Since I'm going to let people you follow have the ability to &lt;code&gt;reply&lt;/code&gt;. I will derive this from the role&lt;code&gt;profile#followed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shout#mentioned&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Setting Up
&lt;/h4&gt;

&lt;p&gt;There are a bunch of ways of setting this up. The &lt;code&gt;README.md&lt;/code&gt; of my repo will explain it thoroughly, but I'll be brief here and just explain things that might be hard to grasp at first. We'll be using this &lt;code&gt;permit&lt;/code&gt; object below. Remember to not use this on the frontend.&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;Permit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;permitio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permit&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;Permit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PERMIT_SDK_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pdp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PERMIT_PDP_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PERMIT_API_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.permit.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here's how you would programmatically create an &lt;code&gt;admin&lt;/code&gt; role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shout:delete&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just a note, here's the syntax for permissions: &lt;code&gt;resource#action&lt;/code&gt;. This is basically saying, "Hey, create an admin role, which will be able to do delete on shouts. Since this is a top level role, it will apply to all instances of shouts.&lt;/p&gt;

&lt;p&gt;Here's how you create a &lt;code&gt;shout&lt;/code&gt; resource, along with its available instance roles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createResource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="na"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Resource roles are used to define the permissions for each role on the resource&lt;/span&gt;
    &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;shouter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Shouter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reply&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;replier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Replier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reply&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;mentioned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mentioned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reply&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see I've created 3 unique roles here. Even though they have the same permissions, I've decided to separate them just in case I need to add more actions. Basically this code says "Hey, I want to create a resource called 'shout'. It can have 2 actions performed on it: delete and reply. A user can be a replier, mentioned, and a shouter. All 3 of which will be able to perform all available actions.&lt;/p&gt;

&lt;p&gt;Here's how you would create a relationship between resources. This is very important. Role derivations are extremely powerful, and relations are needed for it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resourceRelations&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shout&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Parent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subject_resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profile&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above snippet basically is saying, "Hey, create a relation for shout. The subject of this relationship is profile. I want profiles to be parents of shouts." &lt;/p&gt;

&lt;p&gt;I got confused by this at first, so I'll try to explain it just in case you are too. Basically, imagine in your head what relationship between two resources do you want to have.&lt;/p&gt;

&lt;p&gt;Here we have &lt;code&gt;shout&lt;/code&gt; and &lt;code&gt;profile&lt;/code&gt;. We'll I want to connect these as such that a &lt;code&gt;profile&lt;/code&gt; will have many instances of &lt;code&gt;shout&lt;/code&gt;. Let's go with "Profile is the parent of shouts".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;subject_resource&lt;/code&gt; is the &lt;code&gt;name&lt;/code&gt; of &lt;code&gt;first parameter of create&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Above is how it would translate to parameters.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORANT: the &lt;code&gt;name&lt;/code&gt; parameter can be anything. It doesn't even have to realistically describe the relationship (but it should). So I could've put &lt;code&gt;owns&lt;/code&gt; as the &lt;code&gt;name&lt;/code&gt; parameter just the same. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, creating resource derivations. I want EVERYONE who follows a user to be able to reply to all their &lt;code&gt;shouts&lt;/code&gt;. So, let's derive &lt;code&gt;profile#followed&lt;/code&gt; to &lt;code&gt;shout#replier&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORANT: relationships are integral to derivative relationships. The above rule I want to implement means that users will have the &lt;code&gt;shout#replier&lt;/code&gt; role ONLY if he has &lt;code&gt;profile#followed&lt;/code&gt; on a profile AND if that profile is a parent of the &lt;code&gt;shout&lt;/code&gt; in question.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's how I created that derivation in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resourceRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;replier&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;granted_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;users_with_role&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;linked_by_relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;on_resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;followed&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="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;Pretty simple. This is basically saying, "Hey, update this shout#replier role you created earlier and grant this role to users with the followed role on a profile. And only grant this replier role IF the profile has a parent relationship with the shout in question"&lt;/p&gt;

&lt;h3&gt;
  
  
  Syncing Users
&lt;/h3&gt;

&lt;p&gt;Obviously permit.io doesn't manage your authentication. You will need a your own or a one from a third party. Like Clerk. However, this does not mean you can neglect managing users in permit.io.&lt;/p&gt;

&lt;p&gt;Permit.io doesn't automatically know your users from your authentication system. Let's use Clerk for example as that's the library I'm using in my app.&lt;/p&gt;

&lt;p&gt;After you've created your user in Clerk. You need also create them in Permit.io. You can do this in your login/signup logic, or you can create a separate process entirely. You know how in Twitter/X, after you sign up with your email or Google account, you still have to fill up things like username, name, etc? I'm going to take this opportunity to sync my users to permit.io&lt;/p&gt;

&lt;p&gt;So, if a user has logged in using Clerk, and there isn't a matching entry in Permit.io, I'm going to force them to complete their profile.&lt;/p&gt;

&lt;h4&gt;
  
  
  Middleware in NextJs
&lt;/h4&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="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;clerkMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;userId&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;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&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="nf"&gt;isProtectedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// get project_id and environment_id&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;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;environment_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permitApi&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v2/api-key/scope&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;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&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;res&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="c1"&gt;// check if user's profile is complete (based on their existence in Permit)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isProfileComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/v2/facts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;isProfileComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;isProfileComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// if it's a public route, the user is signed in and the profile is complete, redirect to home&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPublicRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isProfileComplete&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/home`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// if the user has not completed their profile and is trying to access a protected route, redirect to profile creation page&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isProfileComplete&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isProtectedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/profile/create&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/profile/create`&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;Here's a snippet of my middleware. This will run on most pages you go to on PermiShout. Basically, what it does is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get the currently logged in user's id using &lt;code&gt;(await auth()).userId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If the user has not even registered or signed in to Clerk, we can just protect our protected routes with &lt;code&gt;await auth.protect()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If the user has been authenticated (by clerk), we check if they have a matching entry in permit.io
We check this by using the following code:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// note that `permitApi` is just an instance of `axios`&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/v2/facts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;If that api call succeeds, that means the user has a matching &lt;code&gt;user&lt;/code&gt; in Permit.io. We're good. The user can access any protected and non public-only routes.&lt;/li&gt;
&lt;li&gt;IF the api call fails, that means the user has no matching &lt;code&gt;user&lt;/code&gt;. Permit.io doesn't know about this user. Now we force the user to go to the &lt;code&gt;/profile/create&lt;/code&gt; route. Where I will be forcing them to create an account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now after filling in some info, mainly username, name, country, and year born, we handle&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SYNCING USERS&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;in the backend or a route handler as we're using NextJS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&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;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;clerkClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;username&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="nx"&gt;yearBorn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;splitName&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;clerkClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;lastName&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createdUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;syncUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&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="nx"&gt;emailAddresses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;emailAddress&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;yearBorn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yearBorn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;country&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;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleAssignments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createdUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resource_instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`profile:profile_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;createdUser&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll explain a little bit what's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, we get the currently authenticated user from Clerk (reminder that this is not a user from Permit.io -- That's what we're here for!)&lt;/li&gt;
&lt;li&gt;Then we get all the extra attributes we want to put in Permit.io&lt;/li&gt;
&lt;li&gt;Then--and Permit.io makes this REALLY easy for us-- we do &lt;code&gt;permit.api.syncUser&lt;/code&gt;.
Remember to match the user key for Permit.io with the &lt;code&gt;id&lt;/code&gt; of your Clerk user. You don't necessarily have to match it, but it will be infinitely HARDER if you don't. For example, if you decide to use only the first 10 characters of your Clerk &lt;code&gt;id&lt;/code&gt;, you will need to get those first 10 every time you reference your Permit.io user. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also add &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;first_name&lt;/code&gt;, and &lt;code&gt;last_name&lt;/code&gt;. This will also be your opportunity to add any extra attributes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: I said Permit.io makes this REALLY easy because you don't have to differentiate between an existing user and one that hasn't been created. &lt;code&gt;syncUser&lt;/code&gt; will handle it. If it exists, it will overwrite it (sync it), and if it doesn't, it will create it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Now, since I have the &lt;code&gt;profile&lt;/code&gt; resource set up. I want to also associate each user with one. So that's where this line is for:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleAssignments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createdUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resource_instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`profile:profile_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;createdUser&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this code does two-fold. First, it creates a resource instance of &lt;code&gt;profile&lt;/code&gt; with the key &lt;code&gt;profile_${created_user_key}&lt;/code&gt;. Something important to remember is that &lt;code&gt;resource_instance&lt;/code&gt; has to be a string in the format &lt;code&gt;resource:instance_key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Secondly, this code also assigns a role to user with the &lt;code&gt;createdUser&lt;/code&gt; key and assigns a role &lt;code&gt;owner&lt;/code&gt; to it (the &lt;code&gt;profile&lt;/code&gt; instance).&lt;/p&gt;

&lt;p&gt;So now, this particular user is officially the owner of their profile and all the access control that comes with that instance role on that particular profile as well as any role derivations.&lt;/p&gt;

&lt;p&gt;I just wanted to mention this because making role assignment and instance created in one function is really neat!&lt;/p&gt;

&lt;h3&gt;
  
  
  Doing Checks
&lt;/h3&gt;

&lt;p&gt;An important thing to note is that checks should always be done, as in you will need to query them. Permit.io doesn't know about your code and what you're doing, they just make writing the code for checking permissions way easier. Here's an example check I do whenever a user is deleting a &lt;code&gt;shout&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUserAllowedToDelete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&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;Really simple isn't it? Instead of checking multiple ALL the scenarios where a user will be allowed to delete a particular shout, we just ask Permit.io! Here, it will return &lt;code&gt;true&lt;/code&gt; if user is an &lt;code&gt;admin&lt;/code&gt; or if user is the &lt;code&gt;owner&lt;/code&gt; of the &lt;code&gt;shout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These two conditions are simple. But imagine if you have multiple unique ones you have to keep track of. Imagine if a user can also be a low level moderator to delete. You have to write that check and add them to ALL parts of your code who need the check.&lt;/p&gt;

&lt;p&gt;Anyway, if &lt;code&gt;isUserAllowedToDelete&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, we proceed to delete the instance using this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resourceInstances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`shout:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy!&lt;/p&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;permit-fe-sdk&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is my favorite part. Honestly figuring out how the whole thing works felt great! Basically, this library helps you in the front end in determining whether or not a user is allowed to do something.&lt;/p&gt;

&lt;p&gt;A similar concept exists in Laravel's blade directives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@can('update', $post)
  &amp;lt;button&amp;gt;Delete&amp;lt;/button&amp;gt;
@endcan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Permit.io works with CASL, you can achieve the same function. But this time this is all happening in the frontend -- SAFELY!&lt;/p&gt;

&lt;p&gt;Here are the steps. These were really hard for me to grasp and figure out, so hopefully I can explain it well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the necessary packages
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @casl/ability @casl/react permit-fe-sdk permitio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a POST route handler where the &lt;code&gt;permit-fe-sdk&lt;/code&gt; will be calling for permission checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Based on my own research, you want to create POST routes if you're trying to get bulk permissions. Meaning, you check for many permissions at once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's mine (I'll be focusing on bulk permissions):&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;searchParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resourcesAndActions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No userId provided.&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&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;checkPermissions&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="na"&gt;resourceAndAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;userAttributes&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;PermishoutUserAttributes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;resourceAttributes&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="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;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&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;resourceAttributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nx"&gt;resourceAndAction&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;resourceType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resourceKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;attributes&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="p"&gt;},&lt;/span&gt;
        &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resourceType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resourceKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resourceAttributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&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="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permittedList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;resourcesAndActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkPermissions&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;permittedList&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &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;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Permission check error:&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal Server Error&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;I find it useful to mention the end goal of this route. It's basically to return an object with &lt;code&gt;permittedList&lt;/code&gt; as an array of booleans. Now you could easily just return a list of 100 &lt;code&gt;true&lt;/code&gt;s and you'd pass all the check. But that's not what we're here for right?&lt;/p&gt;

&lt;p&gt;We'll get to the frontend a little later, but basically we want to map &lt;code&gt;resourcesAndActions&lt;/code&gt; to permission results. This variable will be an array of resources and actions queried from the frontend.&lt;/p&gt;

&lt;p&gt;Each array element will have &lt;code&gt;resouce&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;userAttributes&lt;/code&gt; and &lt;code&gt;resourceAttributes&lt;/code&gt;. You'll want to accurately test for these permissions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: notice that I split the resource and resource key? This is because you supply them from the frontend in this format &lt;code&gt;resource:resource_key&lt;/code&gt;. And you get it in this route as is, so you have to manually split it and then put the resource in &lt;code&gt;type&lt;/code&gt; and the key in &lt;code&gt;key&lt;/code&gt;. This stumped me for a while.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay so based on all the actions and resources we do &lt;code&gt;permit.check&lt;/code&gt; so it will return the boolean array in the correct order.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: Remember that order is very very important. You can't just be randomly doing &lt;code&gt;permitionList.reverse()&lt;/code&gt;. The frontend wouldn't be accurate anymore in determining permissions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay so we have return that list of booleans. Now what?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup the frontend to be able to use that route
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;defaultActionResources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActionResourceSchema&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="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shout&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AbilityContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ability&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AbilityTuple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MongoQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;setActionResources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Dispatch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SetStateAction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ActionResourceSchema&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAbility&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="nx"&gt;ability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AbilityContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;useAbility must be used within an AbilityProvider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AbilityLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&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;isSignedIn&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;=&lt;/span&gt; &lt;span class="nf"&gt;useUser&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;ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAbility&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ability&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AbilityTuple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MongoQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ability&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;actionResources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setActionResources&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="nx"&gt;ActionResourceSchema&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;defaultActionResources&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// reset it first&lt;/span&gt;
      &lt;span class="nf"&gt;setAbility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ability&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;isSignedIn&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;permit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Permit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;loggedInUser&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;backendUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/permit/check&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="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;reset&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;allActionResources&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="nx"&gt;actionResources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;defaultActionResources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadLocalStateBulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allActionResources&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;caslConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;permitState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCaslJson&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;caslAbility&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="nx"&gt;caslConfig&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;caslConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caslConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ability&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;setAbility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caslAbility&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isSignedIn&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="nx"&gt;actionResources&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AbilityContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setActionResources&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;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/AbilityContext.Provider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Basically we want to provide an &lt;code&gt;Ability&lt;/code&gt; object throughout the whole app. I'm using &lt;code&gt;Context.Provider&lt;/code&gt; for this.&lt;/p&gt;

&lt;p&gt;Remember actions and resources from the route? That maps directly to the ones in this context provider.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: The check you will be able to do directly corresponds to what you supply to &lt;code&gt;await permit.loadLocalStateBulk(allActionResources)&lt;/code&gt;. If you supply it with one pair of resource and action, it will not be able to accurately check for others.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have created a local state called &lt;code&gt;actionResources&lt;/code&gt;, which will cause the &lt;code&gt;useEffect&lt;/code&gt; to update and load the local state with the latest permission.&lt;/p&gt;

&lt;p&gt;I have some default permissions I will always want to check: admin permissions. That's why I have some default action resources.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: Make sure to reset it also. I've found problems if I just call &lt;code&gt;loadLocalStateBulk&lt;/code&gt; over and over.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it! Just make sure you update the &lt;code&gt;ability&lt;/code&gt; variable with the most recent permissions. Now you can use &lt;code&gt;useAbility&lt;/code&gt; to get that ability and perform checks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Actually using it in the frontend
Let's focus on shout permissions. Admin will always be able to &lt;code&gt;delete&lt;/code&gt; &lt;code&gt;shout&lt;/code&gt;s so let's put that in the default array.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, shouts need fine-grained control as there are roles and permissions exclusive to single instances of &lt;code&gt;shout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's how how handled it. Remember that I exposed &lt;code&gt;setActionResources&lt;/code&gt; in that provider and that if that array is updated, it will update local permissions. We'll that's what I'm going to do in the &lt;code&gt;home&lt;/code&gt; page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setActionResources&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAbility&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;shoutActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActionResourceSchema&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shouts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`shout:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`shout:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nf"&gt;setActionResources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shoutActions&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;shouts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setActionResources&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's it. This will update all our permission in the frontend so we can use them. In each &lt;code&gt;shout&lt;/code&gt; I want to have a delete button for users who have the permission. I can use the &lt;code&gt;Can&lt;/code&gt; component from CASL:&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;Can&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@casl/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Can&lt;/span&gt; &lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`shout:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ghost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex items-center gap-1 text-red-400 hover:text-red-500 ml-auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopPropagation&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;setShoutToDeleteKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setShoutToDeleteKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}}&lt;/span&gt;
 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Trash&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Delete&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Can&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's all I need to do. Just supply it with the action and the resource as well as the &lt;code&gt;ability&lt;/code&gt; object I mentioned.&lt;/p&gt;

&lt;p&gt;You can also do a standard inline check like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;permitState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;permit-fe-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canReply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replyMode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ShoutReplyType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EVERYONE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;permitState&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`shout:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Sorry for the tangent. I was super excited getting this working on my own, especially the front end sdk. Hopefully they will keep working on it because I love Laravel blade directives. And this is exactly that in &lt;code&gt;React&lt;/code&gt;!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>permitchallenge</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Dev Dash - Recreating Dev.to with Kendo React</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Sun, 23 Mar 2025 12:15:03 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/dev-dash-recreating-devto-with-kendo-react-484b</link>
      <guid>https://forem.com/ansellmaximilian/dev-dash-recreating-devto-with-kendo-react-484b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kendoreact"&gt;KendoReact Free Components Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;When scrolling through the &lt;a href="https://www.telerik.com/kendo-react-ui" rel="noopener noreferrer"&gt;Kendo React docs&lt;/a&gt;, I thought this component list would be perfect for an app that focuses on data. Dev.to just so happens to provide an API for me to fetch its data. And I thought it would be fitting, given where this challenge is happening.&lt;/p&gt;

&lt;p&gt;So, I built Dev Dash. It's basically a simple redesign version of Dev.to, but I also attempted to extend it with some unique features not present in the original website, with the help of Kendo React. I was however limited by the API itself.&lt;/p&gt;

&lt;p&gt;You would login, submit a Dev.to API key, which you could find &lt;a href="https://dev.to/settings/extensions"&gt;here&lt;/a&gt;. Then, you'll be able to use Dev Dash's features, which are:&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard
&lt;/h3&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%2Fsrr7p59d4a0d8cuomasy.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%2Fsrr7p59d4a0d8cuomasy.png" alt="Image description" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, you'll see a some basic data about the currently authenticated Dev user (through the API key), like name, username, profile picture, etc. There's even a countdown to your next birthday celebrating when you joined Dev.to.&lt;/p&gt;

&lt;p&gt;Here's where Kendo React will do the heavy lifting. I've arranged the dashboard in what's called a &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/tilelayout" rel="noopener noreferrer"&gt;TileLayout&lt;/a&gt;. It's super cool! With just a little bit of setup, you can have a layout composed of tiles which you can rearrange and even resize.&lt;/p&gt;

&lt;p&gt;I added to this by automatically saving the arrangement of the dashboard every time a change happens. So that when you go back to this later, it will remember your setup and you won't have to rearrange and resize the tiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feed
&lt;/h3&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%2F04ua1ogklmpm3fy97z7q.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%2F04ua1ogklmpm3fy97z7q.png" alt="Image description" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is just your regular feed, as seen in the original Dev.to site, with a couple easy filtering features, by username or by tags.&lt;/p&gt;

&lt;p&gt;The star in my opinion is the &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/card" rel="noopener noreferrer"&gt;Card&lt;/a&gt; component. I didn't have to much. I just plopped &lt;code&gt;&amp;lt;Card/&amp;gt;&lt;/code&gt; along with its accompanying components such as &lt;code&gt;&amp;lt;CardHeader/&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;CardFooter/&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Posts
&lt;/h3&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%2Fehg1ao8sqoio3dllif2f.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%2Fehg1ao8sqoio3dllif2f.png" alt="Image description" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here you'll see your posts, published or unpublished. I use &lt;a href="https://www.telerik.com/kendo-react-ui/components/grid" rel="noopener noreferrer"&gt;Data Grid&lt;/a&gt; along with a &lt;a href="https://www.telerik.com/kendo-react-ui/components/datatools" rel="noopener noreferrer"&gt;Pager&lt;/a&gt; for pagination.&lt;/p&gt;

&lt;p&gt;Here you'll view a paginated list of your posts and you'll also be able to pin your favorite uploads, which then will be displayed in your dashboard.&lt;/p&gt;

&lt;p&gt;Now, Data Grid is available in the free tier. But there are many premium features you can add to with with a license. I actually used one particular premium feature just for comparison. And that feature is &lt;code&gt;custom cell renderer&lt;/code&gt;. I use this to insert a "pin" button into a column for each row.&lt;/p&gt;

&lt;h3&gt;
  
  
  Library Feature
&lt;/h3&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%2Flqybns5uytc1oicu1npu.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%2Flqybns5uytc1oicu1npu.png" alt="Image description" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This section is where I extend upon what's already available through Dev.to by allowing users to organize their reading list by "library sections", identified by color and name.&lt;/p&gt;

&lt;p&gt;I use the &lt;a href="https://www.telerik.com/kendo-react-ui/components/inputs/colorpalette" rel="noopener noreferrer"&gt;ColorPalette&lt;/a&gt; component so the user can choose from a  list of colors and create a library section for you to put your reading list items in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo
&lt;/h3&gt;

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

&lt;p&gt;&lt;a href="https://devdashkendo.netlify.app/" rel="noopener noreferrer"&gt;Live App&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AnsellMaximilian/dev-dash" rel="noopener noreferrer"&gt;Repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  KendoReact Experience
&lt;/h2&gt;

&lt;p&gt;Overall I really enjoyed recreating and extending Dev.to using Kendo React. I probably worked on this for about a week. And while there are a lot of things that I to implement that I didn't get to deploy, I feel like I did a lot in the timeframe I developed in.&lt;/p&gt;

&lt;p&gt;It's definitely thanks to the very complete list of ready free components by Kendo React. I was expecting the free tier to be less extensive to be honest but was very pleasantly surprised.&lt;/p&gt;

&lt;p&gt;I would also like to mention how easy it was switch themes. First I used the default, then I opted for Bootstrap. All it took was changing one line.&lt;/p&gt;

&lt;p&gt;Here are the list of free components and how I used them (as well as how I feel about them):&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/buttons" rel="noopener noreferrer"&gt;Buttons&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Of course we have buttons. I used these all over the place. Here's some unique features that I used that I really liked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Toggleable buttons&lt;/li&gt;
&lt;li&gt;Easily adding icons into a button by just supplying a prop into the component without worrying of putting classes for flex, etc.&lt;/li&gt;
&lt;li&gt;Separating &lt;code&gt;themeColor&lt;/code&gt; (primary, secondary, etc.) and &lt;code&gt;fillMode&lt;/code&gt; (flat, outline, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/indicators/loader" rel="noopener noreferrer"&gt;Loader&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;I also used these everywhere. I like it. I used the large version when loading a whole page and the small version to load independent components within a page.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/common/svgicon" rel="noopener noreferrer"&gt;SvgIcon&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This was really useful. There's a whole lot of icons available as SVGs. It could be expanded, but 95% of my icon needs I got from this.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/inputs/textbox" rel="noopener noreferrer"&gt;TextBox&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Kendo React provides a lot of versions of text inputs; however I got all I needed from &lt;code&gt;TextBox&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/appbar" rel="noopener noreferrer"&gt;AppBar&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;I love the associated &lt;code&gt;AppBarSpacer&lt;/code&gt; component. It's really useful for arranging your &lt;code&gt;AppBar&lt;/code&gt; without much work.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/drawer" rel="noopener noreferrer"&gt;Drawer&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This is really useful for navigation. The setup is easy. And of course: I like that I don't have to do much to achieve a collapsible sidebar!&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/avatar" rel="noopener noreferrer"&gt;Avatar&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Standard avatar component. I didn't use any of the special properties Kendo React provided, but it was good enough to display the user's profile picture at the end of the &lt;code&gt;AppBar&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/popup" rel="noopener noreferrer"&gt;Popup&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;I used this for showing extra info as well as a logout button when a user clicks on their avatar on the app bar.&lt;/p&gt;

&lt;p&gt;Again, the it serves its purpose because I don't want to have to deal with anchoring things in elements; and this allowed me to avoid exactly that. That's always a plus.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/card" rel="noopener noreferrer"&gt;Card&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Standard card component with its associated child components such as &lt;code&gt;CardHeader&lt;/code&gt; and &lt;code&gt;CardFooter&lt;/code&gt; made it easy to display posts and related info.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/tilelayout" rel="noopener noreferrer"&gt;TileLayout&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This is by far my &lt;em&gt;favorite&lt;/em&gt; component that I discovered while developing this application. &lt;/p&gt;

&lt;p&gt;Using a tile layout you can separate your display into multiple tiles, and each individual tile can be rearranged and resized. I took it even further by allowing users to save their configuration in the cloud.&lt;/p&gt;

&lt;p&gt;I think this makes configurable dashboards really easy.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/dropdowns/autocomplete" rel="noopener noreferrer"&gt;Autocomplete&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Standard auto complete component. No complaints.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/indicators/skeleton" rel="noopener noreferrer"&gt;Skeleton&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Same with the skeleton components. Though I did notice there were a lot of variations, so that's a +.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/inputs/colorpalette" rel="noopener noreferrer"&gt;ColorPalette&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;There are many available color input components. This is the one I chose. Simple. I like it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/dialogs" rel="noopener noreferrer"&gt;Dialog&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Standard dialog. No complaints.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/grid" rel="noopener noreferrer"&gt;Data Grid&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The free version I think is good enough. I used a custom cell renderer, which was a premium function.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/datatools" rel="noopener noreferrer"&gt;Pager&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Free pagination tools. Cool!&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.telerik.com/kendo-react-ui/components/layout/tabstrip" rel="noopener noreferrer"&gt;TabStrip&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Tab layout. No complaints.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kendoreactchallenge</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cycle Doodle - Web Game Powered by DevCycle</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Sun, 15 Dec 2024 12:38:53 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/cycle-doodle-2kh7</link>
      <guid>https://forem.com/ansellmaximilian/cycle-doodle-2kh7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/devcycle"&gt;DevCycle Feature Flag Challenge&lt;/a&gt;: Feature Flag Funhouse&lt;/em&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What I Built
&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%2Fiu5ybpm9x6vnoeoee5og.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%2Fiu5ybpm9x6vnoeoee5og.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;br&gt;
I built a little game called Cycle Doodle for this challenge. Here's a little background on how I came up with this web game:&lt;/p&gt;

&lt;p&gt;This dev challenge called for us to explore the capabilities of &lt;a href="https://www.devcycle.com/" rel="noopener noreferrer"&gt;DevCycle&lt;/a&gt; and what it can do for our apps. So, I thought I would further challenge myself in building this app by exclusively utilizing only DevCycle's services/features. So besides a frontend framework, which will be NextJs for this, I won't be using any other external third party services like a database, socket, etc.&lt;/p&gt;

&lt;p&gt;Cycle Doodle is a simple web game in which you'll join one of three teams and then either draw a simple prompt on a grid with simple colors, guess what other teams have drawn, or vote/rate those other teams' drawings.&lt;/p&gt;

&lt;p&gt;Your team will cycle through these roles every time a member of a team does one of these actions (draw, guess, vote), hence the name "Cycle Doodle".&lt;/p&gt;

&lt;p&gt;To encourage users to choose their teams, each team will have a distinct advantage over the other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grid Goblins: Other teams get 100 cells in a grid, while this one gets 144, allowing for more details.&lt;/li&gt;
&lt;li&gt;Edit Emperors: Unlike other teams, this one can edit and draw over colored cells in the grid.&lt;/li&gt;
&lt;li&gt;Paint Paladins: Other teams get 5 colors to pain their cells, while this team gets 10.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then users can view the gallery to see all the drawings by all the teams. The separation and advantages will allow us to see the difference in drawing quality and creativity between all the teams given a different set of tools.&lt;/p&gt;

&lt;p&gt;A few chosen users (myself) can act as admins to edit the list of possible prompts.&lt;/p&gt;
&lt;h2&gt;
  
  
  How DevCycle Was Used
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Assigning Users to Teams (Audience)
&lt;/h3&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%2F8l46y2aixh9i3ri3ojzt.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%2F8l46y2aixh9i3ri3ojzt.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DevCycle allows you to create user groups called &lt;a href="https://docs.devcycle.com/platform/feature-flags/targeting/audiences" rel="noopener noreferrer"&gt;Audiences&lt;/a&gt;. I thought this would be perfect feature to use for my team assignment feature. This not only allows me to group each user to a different team, but also serve values differently based on what group a user is in. This will be useful in differentiating each team by their advantages.&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%2Fktbppg07ou6urtqce0zm.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%2Fktbppg07ou6urtqce0zm.PNG" alt="Teams" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, besides "Admin", I have three separate audiences denoting which team a user could belong too.&lt;/p&gt;

&lt;p&gt;Then, I also made a &lt;a href="https://docs.devcycle.com/platform/feature-flags/features" rel="noopener noreferrer"&gt;Feature&lt;/a&gt; to serve certain values based on which team a user is on.&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%2Fy4qvaklk2uzz89ikvf24.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%2Fy4qvaklk2uzz89ikvf24.PNG" alt="Image description" width="800" height="286"&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%2Fpknwe8schhdbufwm06mv.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%2Fpknwe8schhdbufwm06mv.PNG" alt="Image description" width="598" height="787"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have also made 3 different configurations for a variable within that feature. Further, I set the targeting rules as such that different audiences will get served different values. Luckily for me, DevCycle allows for the serving of many kinds of values, like JSON. For example, Grid Goblins will get:&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;"gridSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canEdit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"colors"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;colors&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;Now, how do I actually assign these users to their chosen teams without having to go to the DevCycle dashboard each time a user chooses? Fortunately, DevCycle provides a very complete list of APIs that I can use. One of those allows me to update/create audiences.&lt;/p&gt;

&lt;p&gt;So, since I'm already using NextJS, I can use its route handlers to securely call the &lt;a href="https://docs.devcycle.com/management-api/#tag/Audiences/operation/AudiencesController_findOne" rel="noopener noreferrer"&gt;the api&lt;/a&gt; to update which users belong in which audience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cycling Team Roles
&lt;/h3&gt;

&lt;p&gt;Now that there are dedicated audiences for certain teams, I can reuse this as much as I want. Including in this little feature of the game. As I've mentioned, each time will have a different role, namely, Drawer, Guesser, and Voter; each time a user does a particular action for their role, the teams' roles will shift.&lt;/p&gt;

&lt;p&gt;Well, the way I did this is the same as before -- with a feature. I made another feature called "Team Role". And instead of JSON, I will serve different strings for different Audiences. For example, at one point in time, Edit Emperors will be served &lt;code&gt;guesser&lt;/code&gt;, Grid Goblins will be served &lt;code&gt;drawer&lt;/code&gt;, etc.&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%2F46aa9439zbhhx4yyk02j.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%2F46aa9439zbhhx4yyk02j.PNG" alt="Image description" width="800" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, for shifting the roles, I did a similar thing as the assigning of teams. APIs! Again, DevCycle generously provided me with &lt;a href="https://docs.devcycle.com/management-api/#tag/Feature-Configurations/operation/FeatureConfigsController_findAll" rel="noopener noreferrer"&gt;the perfect API&lt;/a&gt;. By hitting this endpoint, I can programmatically and dynamically cycle the roles of the teams.&lt;/p&gt;

&lt;h4&gt;
  
  
  Real Time Updates
&lt;/h4&gt;

&lt;p&gt;DevCycle provides real time updates to their feature variables. So, right after I cycle through the team roles through the API, I don't even have to manually update it on the frontend. DevCycle will give the most recent value and the app will automatically pick up on the change and update it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Saving, Fetching, and Updating Prompts
&lt;/h3&gt;

&lt;p&gt;I mentioned in the beginning that I challenged myself to not use any external services like databases. Well, I didn't. But how do I store the prompts, drawings, guesses, etc. persistently and share it across all users? Well, Features to the rescue again!&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%2Fqo7a6s9ribny1synt5lj.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%2Fqo7a6s9ribny1synt5lj.PNG" alt="Image description" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unlike the other features before, I will serve this variable to all users so they all have access to it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Drawing
&lt;/h4&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%2Fafztmy53h1n4cofkojl2.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%2Fafztmy53h1n4cofkojl2.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When your team currently has the "Drawer" role, this is what you will see. Simply fill up the grid with the colors you have and submit it. This will hit another different API provided by DevCycle, which is the &lt;a href="https://docs.devcycle.com/management-api/#tag/Features/operation/FeaturesController_remove" rel="noopener noreferrer"&gt;update feature API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I simply hit this API to update the "prompts" feature variable with how the user has drawn in the grid. Then I also cycle the team roles.&lt;/p&gt;

&lt;h4&gt;
  
  
  Guessing
&lt;/h4&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%2Fzoxesuzjy2j5oyc799h9.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%2Fzoxesuzjy2j5oyc799h9.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When your team currently has the "Guesser" role, this is what you will see. Try to guess what this other team has drawn.&lt;/p&gt;

&lt;h4&gt;
  
  
  Voting/Scoring
&lt;/h4&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%2Fzksss6tazbf56oldybc9.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%2Fzksss6tazbf56oldybc9.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When your team currently has the "Voter" role, this is what you will see. Rate the drawing from 1 through 5.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gallery
&lt;/h3&gt;

&lt;p&gt;Users can also view prompts in the gallery. See how other teams drew their prompts and see if it was guessed correctly.&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%2Fsurow0950u1892sqdmaa.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%2Fsurow0950u1892sqdmaa.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization
&lt;/h3&gt;

&lt;p&gt;This I can see being a very important use case for DevCycle. Permissions/Authorizations. I've established that users can draw, guess, and vote on prompts.&lt;/p&gt;

&lt;p&gt;But I want to be able to add/delete/update those prompts as well. And I can't just let any user be able to do that. What do I do to make special privileges on certain users (myself)? Because I don't want to have to update the JSON every time I want to update a prompt.&lt;/p&gt;

&lt;p&gt;Feature and Audience are a great duo, and they've come to the rescue again. What I've done is created a special Audience and included only myself as the sole member.&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%2Fqipoq3g0wwvtkpoe3e6w.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%2Fqipoq3g0wwvtkpoe3e6w.PNG" alt="Image description" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've also created yet another feature called "Is Admin". It's a boolean feature, and I serve &lt;code&gt;true&lt;/code&gt; only to members in the Admin Audience.&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%2Fuw4vit26ymu33mnxmic5.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%2Fuw4vit26ymu33mnxmic5.PNG" alt="Image description" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This way I can determine which content to render and which action to allow based on the served value. For example:&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="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdminPage&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;isAdmin&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;getVariableValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex flex-col justify-center items-center py-16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-3xl font-bold mt-16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Access&lt;/span&gt; &lt;span class="nx"&gt;Denied&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;be&lt;/span&gt; &lt;span class="nx"&gt;an&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;access&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PlayContextProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdminControl&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/PlayContextProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here's the gallery when viewed as an admin. There's a create button on the bottom right and a delete button for each prompt:&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%2F08gudm4rja0bj4ugjv45.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%2F08gudm4rja0bj4ugjv45.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Here's a little demo video for my app:&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/7Yc0D68X1Hs"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here's the live app:&lt;br&gt;
&lt;a href="https://cycle-doodle.vercel.app/" rel="noopener noreferrer"&gt;https://cycle-doodle.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screenshots of the application flow can be viewed above.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Code
&lt;/h2&gt;

&lt;p&gt;Here's my code repository:&lt;br&gt;
&lt;a href="https://github.com/AnsellMaximilian/cycle-doodle" rel="noopener noreferrer"&gt;https://github.com/AnsellMaximilian/cycle-doodle&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My DevCycle Experience
&lt;/h2&gt;

&lt;p&gt;It was my first time using a Feature Flag service, and I have to say it is pretty great.&lt;/p&gt;

&lt;p&gt;I especially like the ability to define audiences. I also love the fact that you can serve many different kinds of values. I know being able to serve JSON values allow me to develop this whacky app without a database, but I really believe it will be useful beyond that; rendering/launching features based on more complex values other than on/off for real applications.&lt;/p&gt;

&lt;p&gt;I also love that it provides real time updates. Really saved me from some headaches in the frontend.&lt;/p&gt;

&lt;p&gt;Targeting rules and the fact that you can rank the order of precedence is also REALLY cool.&lt;/p&gt;

&lt;p&gt;I also bet there will be a lot of useful custom software/extensions made for DevCycle thanks to the provided API. Maybe some in-house solution to integrate DevCycle in people's or companies' own platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Prize Categories
&lt;/h3&gt;

&lt;p&gt;My submission also qualifies for "API All-Star", as it's a major part of the application. I use the APIs extensively: updating audience targeting rules, cycling team roles by updating a feature's targeting rules by audience IDs, updating feature variables, and so on.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>devcyclechallenge</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Break-Time: Digital Time Capsules with Pinata</title>
      <dc:creator>Ansell Maximilian</dc:creator>
      <pubDate>Mon, 14 Oct 2024 04:18:43 +0000</pubDate>
      <link>https://forem.com/ansellmaximilian/break-time-digital-time-capsules-with-pinata-3cpa</link>
      <guid>https://forem.com/ansellmaximilian/break-time-digital-time-capsules-with-pinata-3cpa</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/pinata"&gt;The Pinata Challenge &lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pinata-breaktime.vercel.app/" rel="noopener noreferrer"&gt;Live App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/AnsellMaximilian/break-time" rel="noopener noreferrer"&gt;Github Repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pinata.cloud/" rel="noopener noreferrer"&gt;Pinata (Service used to make this app)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A lot of our footprint we leave in this world are increasingly becoming digital. I won't argue whether that's a bad or good thing, but I will say that our footprint, our memories, that we leave is important regardless if it's digital or physical.&lt;/p&gt;

&lt;p&gt;That's why I decided to build &lt;em&gt;Break Time&lt;/em&gt;. Break Time is a web app where you can make digital time capsules, store your digital memories, and then open them at a later date to look back on those fond memories so they aren't lost.&lt;/p&gt;

&lt;p&gt;I call the time capsules &lt;em&gt;Pinatas&lt;/em&gt;, named after the service used to make this app happen (&lt;a href="https://pinata.cloud/" rel="noopener noreferrer"&gt;Pinata&lt;/a&gt;). But I also decided with this term because it's apt for what it represents, a container with valuables and goodies (memories) inside which you can access by opening said container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Usage
&lt;/h3&gt;

&lt;p&gt;Here I'll give a quick overview of how you would use this app (I'll go into detail in a later section).&lt;/p&gt;

&lt;p&gt;Let's say your best friend, John, and your other best friend, Jill, is about to get married in a few weeks. You get the great idea to start storing memories of the upcoming marriage ceremony up to their 5th anniversary (you're confident they love each other deeply and will last forever).&lt;/p&gt;

&lt;p&gt;You start thinking of creating a time capsule to store those memories and then let them open it at the start of their 5th anniversary to look back on their fond memories.&lt;/p&gt;

&lt;p&gt;However, you realize that even now most of your memories with them are digital (videos, photos, PDFs, SVGs, etc.). And you suspect it will be the same case with their marriage, considering it's 2024.&lt;/p&gt;

&lt;p&gt;So you decide to use &lt;em&gt;Break Time&lt;/em&gt;. You create an account, encourage your circle of friends, including Jill and John themselves to create their own accounts. You invite them to be your friends in the app.&lt;/p&gt;

&lt;p&gt;Then, you create a Pinata (Time Capsule). You name it "John and Jill's 5th Anniversary", you create some description, such as "Hi friends! 🌸 Let’s celebrate the love John and Jill will have built over the next five years by capturing memories, well-wishes, and hopes for their future. When we unlock this capsule later, we’ll look back, laugh, and cherish how far they’ve come. Leave your messages, photos, or videos and help make this anniversary truly unforgettable!". You might even add a little thumbnail of their first date.&lt;/p&gt;

&lt;p&gt;Next you decide who should be able to contribute to this Pinata (Time Capsule). You include all your friends to be contributors. And you set John and Jill as the sole openers of the Pinata, so only they can decide to open it. You set the minimum time to the next five years, so you force yourselves, including John and Jill, to wait and not spoil the memories before the 5th anniversary.&lt;/p&gt;

&lt;p&gt;And then finally, after five years, after you've built up a mountain of memories, you can finally ask John and Jill to open the Pinata. Then you'll be able to download and look back on all those memories with a smile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Usages
&lt;/h3&gt;

&lt;p&gt;Of course the timeline doesn't necessarily have to span 5 years. You can even create Pinatas to store memories of a 1-week camping trip. It's up to you!&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Video
&lt;/h3&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Uad3h0LjhaE"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;a href="https://youtu.be/Uad3h0LjhaE" rel="noopener noreferrer"&gt;Demo Video on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Application Flow
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Home Page
&lt;/h4&gt;

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

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2r4k7whezpu87y6fiwxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2r4k7whezpu87y6fiwxz.png" alt="Image description" width="800" height="436"&gt;&lt;/a&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cacsko6tlzligy9hv9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cacsko6tlzligy9hv9x.png" alt="Image description" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Dashboard
&lt;/h4&gt;

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

&lt;p&gt;This is where most of your data will be listed. On the top you'll see the Pinatas you've created. On the bottom right, you'll see a list of users you've added as friends. In the same place, there's also a form where you can add users as your friends by their usernames.&lt;/p&gt;

&lt;p&gt;On the bottom left, you'll see a list of Pinatas in which others have created but involves you somehow, either you've been added as a contributor, an opener, or both.&lt;/p&gt;
&lt;h4&gt;
  
  
  Create Pinata
&lt;/h4&gt;

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

&lt;p&gt;Here's where you create your Pinata. You can set its title, description, and thumbnail picture.&lt;/p&gt;

&lt;p&gt;You can also set who can contribute to the Pinata once it has been created,  as well as limit when invited users can contribute to the Pinata. You can set when allowed contributors can start contributing as well as when the Pinata will stop accepting contributions.&lt;/p&gt;

&lt;p&gt;Then, you can set attributes related to opening the Pinata. You can set a minimum open time, which will mark the earliest date and time the Pinata can be opened. You can also set who will be required to open this Pinata. You can also omit one of these values. For example, you can set a minimum open time and omit the "openers", this way, Pinatas will open automatically when the time comes. Or you can only include allowed "openers"; this way, the Pinata will open once all the openers have opened the Pinata. Or you can combine include both these values so "openers" can only open the Pinata after the set date time.&lt;/p&gt;

&lt;p&gt;You can exclude yourself from contributing or opening.&lt;/p&gt;
&lt;h4&gt;
  
  
  Viewing Pinata
&lt;/h4&gt;
&lt;h5&gt;
  
  
  Unopened Pinata
&lt;/h5&gt;

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

&lt;p&gt;This is what you'll see when you click on an unopened Pinata. You'll see it's details. This is also where you'll contribute your digital memories/files to the Pinata (if you're part of the contributors).&lt;/p&gt;

&lt;p&gt;You won't be able to view the files you've contributed in this state (you have to open it).&lt;/p&gt;

&lt;p&gt;You'll also open the Pinata in this view. It's in real time so when all users open the Pinata, or the it's just time for the Pinata to open, you'll see the Pinata in the open state. See below.&lt;/p&gt;
&lt;h5&gt;
  
  
  Open Pinata
&lt;/h5&gt;

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

&lt;p&gt;This is the Pinata in the open state. You can no longer contribute files, but now you'll be able to see all those memories you've created and uploaded.&lt;/p&gt;
&lt;h4&gt;
  
  
  Profile Page
&lt;/h4&gt;

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



&lt;p&gt;The full code can be found on my &lt;a href="https://github.com/AnsellMaximilian/break-time" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;br&gt;
&lt;/p&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/AnsellMaximilian" rel="noopener noreferrer"&gt;
        AnsellMaximilian
      &lt;/a&gt; / &lt;a href="https://github.com/AnsellMaximilian/break-time" rel="noopener noreferrer"&gt;
        break-time
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;&lt;a href="https://dev.to/challenges/pinata" rel="nofollow"&gt;The Pinata Challenge&lt;/a&gt;&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Links&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pinata-breaktime.vercel.app/" rel="nofollow noopener noreferrer"&gt;Live App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/AnsellMaximilian/break-time" rel="noopener noreferrer"&gt;Github Repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pinata.cloud/" rel="nofollow noopener noreferrer"&gt;Pinata&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What I Built&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Break Time&lt;/em&gt; is a web app for creating &lt;strong&gt;digital time capsules&lt;/strong&gt; (called Pinatas) to store and cherish memories. Pinatas are opened on a future date to relive those memories, making sure your digital footprints remain meaningful.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Example Usage&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;You can create a Pinata named &lt;strong&gt;"John and Jill’s 5th Anniversary"&lt;/strong&gt;, where friends store messages, photos, or videos leading up to their 5th anniversary. Only John and Jill can open it after five years, unlocking all the memories inside.&lt;/p&gt;
&lt;p&gt;The app allows contributors to add memories, with customizable settings for contribution timelines and who can open the Pinata.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a href="https://youtu.be/Uad3h0LjhaE" rel="nofollow noopener noreferrer"&gt;Demo Video on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Screenshots&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;Home Page&lt;/h4&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/fbc577abab52c6ff36e644fd7994b3c8e8d0bb2524b18d548f37140e9497835b/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f796569656b6b686f333077356c3230346c7871362e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/fbc577abab52c6ff36e644fd7994b3c8e8d0bb2524b18d548f37140e9497835b/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f796569656b6b686f333077356c3230346c7871362e706e67" alt="Home"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;Dashboard&lt;/h4&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/065975a67e814a2b2d728710c95065b220a48ae87aecd451a9db89b2ec8bd191/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f366f627334746a7a646b326173393279743839332e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/065975a67e814a2b2d728710c95065b220a48ae87aecd451a9db89b2ec8bd191/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f366f627334746a7a646b326173393279743839332e706e67" alt="Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;Create Pinata&lt;/h4&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/d80a4fed7d339e85c829466b4da5df86ed49668db789d85b7f89b12569ccf9de/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f6a64316a78326f726938793066346434646239702e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/d80a4fed7d339e85c829466b4da5df86ed49668db789d85b7f89b12569ccf9de/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f6a64316a78326f726938793066346434646239702e706e67" alt="Create Pinata"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tech Stack&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; for frontend and secure route handlers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appwrite&lt;/strong&gt; for authentication and metadata storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinata&lt;/strong&gt; for file storage on IPFS&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Example Code for Route Handlers&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-ts notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;export&lt;/span&gt; &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AnsellMaximilian/break-time" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  More Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Next.js
&lt;/h4&gt;

&lt;p&gt;I'm using Ol' Reliable Next.js as my front end as well as to create a few secure route handlers for handling file upload on the client side, as recommended by Pinata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// getting a one time use key to use in the front end&lt;/span&gt;
 &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;keyData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pinata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keys&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;keyName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;pinning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;pinFileToIPFS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;maxUses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &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;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error creating API Key:&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;I would call this each time I want to upload a file, because Next.js limits on how big a file you can send on the server.&lt;/p&gt;

&lt;p&gt;Similarly, I would call another route handler to get a secure URL for a certain file:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pinata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateways&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSignedURL&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;cid&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="nx"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&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;30&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error creating API Key:&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;h4&gt;
  
  
  Appwrite
&lt;/h4&gt;

&lt;p&gt;I needed authentication as well as a place to store some metadata for the files, so I used Appwrite's authentication and database in conjunction with Pinata, so I can store extra details to files uploaded to Pinata, such as file names, cid, etc.&lt;/p&gt;

&lt;p&gt;So I would fetch a file metadata in Appwrite (in this case, it would be a contribution for a Pinata), then I would use the resulting metadata to retrieve a file from Pinata by its &lt;em&gt;CID&lt;/em&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openFile&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;contribution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Contribution&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// getFileURL calls the route handler that returns a signed URL for a particular file by its CID&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFileUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_blank&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How Pinata was Used
&lt;/h3&gt;

&lt;p&gt;Pinata (the service) was used for this app's main file storage. It stores a Pinata's (the time capsule) thumbnail, a user's profile picture, and, most importantly, digital contributions for the Pinata.&lt;/p&gt;

&lt;p&gt;Here's a simplified version of how I implemented Pinata to upload/replace a user's profile picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gets set on the onChange prop of an input element of type "file"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePictureChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChangeEventHandler&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="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;e&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;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// get the one time key&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;keyRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// upload to pinata&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pinata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// delete the original profile picture (replace) by its file id&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profilePictureFileId&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;delRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/delete-files&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;fileIds&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;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profilePictureFileId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// update metadata on appwrite&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dbId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userProfileCollectionId&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;profile&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="na"&gt;profilePictureFileId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upload&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="na"&gt;profilePictureCid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cid&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;Similar flow was used to upload file contributions as well as uploading Pinata thumbnails.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>pinatachallenge</category>
      <category>webdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
