Building a Fast, Efficient Web App: The Technology Stack of PromptSmithy Explained
Tags: webdev, javascript, react, productivity
Published: 2024-03-26 17:08:07

I’ve written a lot of one-off project, internal scripts for my own use, B2C apps, B2B apps, and everything in between. Every time I start a new project I like to use a new stack to try and diversify my own skillset, and so that if I’m ever tasked with doing another similar project in the future, my knowledge can accelerate my workflow. [PromptSmithy](https://promptsmithy.com/) was slightly different though, as the stack hadn’t changed from other recent projects of mine, however the frontend tooling did greatly. In this article I’m going to break down the stack we used and talk about the new development flow we used for rapid development.

## The Stack

### React + Vite + React Router

We all know what [React](https://react.dev/) is at this point, but why use it with [Vite](https://vitejs.dev/) and [React Router DOM](https://reactrouter.com/en/main) over something like [NextJS](https://nextjs.org/)? 

The reasons are twofold: We didn’t need any of the backend functionality of NextJS, and I wanted something that wouldn’t get in the way of our development with SSR or any other special cases that are only found on NextJS.

On top of that, Vite’s compiler is super fast, supports [Typescript](https://www.typescriptlang.org/) (which we of course used), and built just fine on our host, which was [Cloudflare Pages. Cloudflare Pages](https://pages.cloudflare.com/) is a super fast static website hosting service by Cloudflare, which allows your site to take advantage of their global CDN to make sure your site is as close to your users as possible. It supports nearly any JS framework you could want to use for your site, and can even host plan old HTML if you’re of that persuasion.

React Router is also super minimal and doesn’t get in the way, and provides all the same functionality of NextJS’s static output router without making your compile sizes massive. Our entire built site (before we added all the fancy physics animations) was just a bit over 135KB. 

### Tailwind + shadcn/ui + v0.dev

For development of the UI components, we tried something new. Vercel has this new AI tool called [v0.dev](http://v0.dev) that allows developers to take advantage of [shadcn/ui](https://ui.shadcn.com/) and [Tailwind](https://tailwindcss.com/) using nothing but words, which can then be easily downloaded to your local project using nothing but a simple `npx` command.

![Here is the original example code for a 404 page that I ended up using in the final app!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jxoy51g12efo45ozzr77.png)


Here is the [original example code](https://v0.dev/t/RB8eJs2Kd6R) for a 404 page that I ended up using in the final app! `npx v0 add RB8eJs2Kd6R`

While I have experience with Tailwind and frontend development, I don’t really have the patience to use it. I usually end up using something like [Mantine](https://mantine.dev/), which is a complete component library UI kit, or [Daisy UI](https://daisyui.com/), which is a component library built on top of Tailwind. Shadcn/ui is quite similar to Daisy in this sense, but being able to customize the individual components, since they get installed to your components folder, made development more streamlined and more customizable. On top of that being able to change my components style with natural language thanks to v0 made development super easy and fast. Shadcn may be too minimalist of a style for some, but thanks to all the components being local, you can customize them quickly and easily!

![This is the structure of the project’s components directory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sijgkghiwn9ibl4bru0x.png)

This is the structure of the project’s components directory.

### Supabase

Here the thing that accelerated my development the most: [Supabase](https://supabase.com/). Thanks to its Database, Authentication, and Edge Functions, we were able to rapidly develop the app. Their JS library made development super seamless, and their local development stack made testing a breeze. 

The development process is simple: install their [CLI](https://supabase.com/docs/guides/cli),  run `supabase init`, run `supabase start`. That’s it. (Assuming you have Docker installed that is.)

The database service is pretty self explanatory. Rather than having to write SQL queries in a remote API, hosting that API as well as the database to go with it, you simply create tables with migrations (created using `supabase migrations new name_here`), then you can query the migrations using the frontend API. From there you can configure row level security, which restricts access to specific rows on a per user basis, using either the migrations themselves or using the local UI. I opted for the former so that I could easily apply the migrations. Here is one I wrote for the project.

```sql
create table prompts (
  id bigint primary key generated always as identity,
  user_id uuid not null,
  metaprompt_id bigint references metaprompts(id),
  prompt text,
  variables text,
  title text,
  task text,
  created_at timestamp with time zone default now(),
  updated_at timestamp with time zone default now(),
  public boolean default false
);

alter table prompts
  enable row level security;

create policy "Users can insert their own prompts" on prompts
  for insert with check (auth.uid() = user_id);

create policy "Users can read their own prompts that are private" on prompts
  for select using (auth.uid() = user_id and public = false);

create policy "Users can read all prompts that are public" on prompts
  for select using (public = true);
```

This then got applied locally by running `supabase db reset`, and deployed remotely with `supabase db push`. We could then query on the frontend using the following code:

```tsx
const limit = 100;
const { data, error } = await supabase
      .from("prompts")
      .select("*")
      .eq("public", true)
      .order("created_at", {
        ascending: false,
      })
      .limit(limit);
```

You can see [Supabase’s excellent guides](https://supabase.com/docs/guides/getting-started/quickstarts/reactjs) on how to do this for more information.

We also took advantage of Supabase’s Authentication service that allows us to quickly and effectively log in a user so we can handle authenticated requests (which once Row Level Security is set up is automatic) quickly. Since this was a weekend project sort of app, we went with Supabase Magic Links, which allows our users to log in by simply entering their email and clicking a link that gets sent. Here’s all the code to do that:

```tsx
const email = 'me@example.com';
const { error } = await supabase.auth.signInWithOtp({ email });
```

That’s it! Then all we had to do is have the user click the link (assuming your website URL is configured properly in the Supabase settings) and they were logged in!

Finally we used Supabase Edge Functions to handle payments with Stripe, as well as hold the business logic of PromptSmithy, which primarily just calling Anthropic AI. Edge Functions are written in Deno, which is a NodeJS alternative that I like very much. You create a new edge function by running `supabase functions new name_here` and then deploying with `supabase functions deploy` . You can also run these functions locally for testing (which is what we did along with the Stripe CLI’s webhook feature) with `supabase functions serve` . 

Calling your functions on the frontend is super simple too. Whenever we wanted to call the AI, this is the code we run on the frontend.

```tsx
const input = "Write me an email response to my boss asking for a raise";
const resp = await supabase.functions.invoke<string>(
        "create-task-prompt",
        {
          body: {
            task: input,
            variables: "",
            public: true,
            metapromptID: 1,
          },
        }
      );
```

The value of `resp` would be whatever we responded with, which is always JSON for our application. 

Functions can also be invoked by remote applications, for example Stripe webhooks. If you want this, you’ll need to make sure that JWT verification is disabled for that function, which can be done simply in the `config.toml` in the `supabase`  directory of your project. Here’s an example.

```toml
[functions.stripe-webhook]
verify_jwt = false
```

Now whenever this function is deployed you can check your Edge Functions page for a URL to give to Stripe!

## Conclusion

In conclusion, our choice of stack for PromptSmithy’s project was primarily based on the speed of development and the performance of the end product. Using tools like Vite, React, Supabase, and the innovative v0.dev, we were able to develop rapidly and effectively, resulting in a highly functional and efficient application.

Want to give [PromptSmithy](https://promptsmithy.com/) a try? All new users get **$5 in free credits!**