Create the Application
Project Generation and Structure
The first thing we’ll do is set up our project.
npx degit redwoodjs/sdk/starters/standard applywizecd applywizepnpm installpnpm dev
npx degit redwoodjs/sdk/starters/standard applywizecd applywizenpm installnpm run dev
npx degit redwoodjs/sdk/starters/standard applywizecd applywizeyarn installyarn run dev
Check it out! You’re up and running!
The commands you just ran created a directory called applywize
with the RedwoodSDK project structure.
Inside, the applywize
directory, you should have the following files and folders:
- .dev.vars # environment variables for development
- .env # environment variables
- .env.example # example environment variables
- .gitignore # files to ignore in git
- .prettierrc # prettier configuration
- .wrangler # Cloudflare configuration
Directorymigrations # database migrations
- 0001_init.sql # initial migration
- node_modules # dependencies
- package.json # lists all the dependencies and scripts
- pnpm-lock.yaml # lock file for pnpm
Directoryprisma # prisma configuration
- schema.prisma # database schema
- README.md # project overview
Directorysrc
Directoryapp
- Document.tsx # Main HTML document
- headers.ts # Sets up page headers
Directorypages
- Home.tsx # Home page
Directoryuser
- functions.ts # authentication functions
- Login.tsx # Login page
- routes.ts # authentication routes
Directoryshared
- links.ts # list of links
- client.tsx # initializes the client
- db.ts # database client
Directoryscripts
- seed.ts # seed the database
- session # sets up sessions for authentication
- worker.tsx # Cloudflare worker
- tsconfig.json # TypeScript configuration
Directorytypes
- vite.d.ts # Vite types
- vite.config.mts # Vite configuration
- worker-configuration.d.ts # Cloudflare worker configuration
- wrangler.jsonc # Cloudflare configuration
Setting up our Database
The first time you run pnpm dev
, it creates the .wrangler
directory. This directory is used by Cloudflare’s Miniflare for storing assets. It contains your local database, caching information, and configuration data.
First, we can create a D1 database that’s connected to our Cloudflare account using the following command:
npx wrangler d1 create applywize
Here, applywize
is the name of our database.
Copy the database ID provided and paste it into your project’s wrangler.jsonc
file:
{ "d1_databases": [ { "binding": "DB", "database_name": "__change_me__", "database_id": "__change_me__" } ]}
While we’re in our wrangler.jsonc
file, let’s also update our worker name:
"name": "applywize",
and the APP_name
:
"vars": { "APP_NAME": "applywize"},
Generating Secret Keys
Next, we need to generate a strong SECRET_KEY
for signing session IDs. You can generate a secure random key using OpenSSL. This command will generate a 32-byte random key and encode it as a base64 string.
openssl rand -base64 32
Then set this key as a Cloudflare secret, where SECRET_KEY
is the name of the key. It will prompt us for the random string we generated.
npx wrangler secret put SECRET_KEY
For production, set your domain as the RP_ID
via Cloudflare secrets:
npx wrangler secret put RP_ID
When prompted, enter your production domain (e.g., my-app.example.com).
Setting up Cloudflare Turnstile
Next, let’s set up Cloudflare Turnstile for bot protection.
- Visit Cloudflare Turnstile Dashboard.
- Create a new Turnstile widget:
- Set Widget Mode to Invisible. This will prevent users from having to solve a challenge or see the widget loading bar.
- Add your application’s hostname to Allowed hostnames, e.g., my-project-name.example.com
- Mark “Would you like to opt for pre-clearance for this site?” as No.
- After you click the Create button, you’ll be redirected to the confirmation screen with your Site Key and Secret Key.
- Copy your Site Key and paste it into your application’s
Login.tsx
. This is defined on line 18.src/app/pages/auth/Login.tsx const TURNSTILE_SITE_KEY = "<YOUR_SITE_KEY>"; - Set your Turnstile Secret Key via Cloudflare secrets for production:
When prompted, enter your Turnstile Secret Key.
Terminal window npx wrangler secret put TURNSTILE_SECRET_KEY - Our starter kit already has a couple of tables set up, to support the authentication process. This will set up Prisma and create a
.wrangler
file within our project and setup the Cloudflare d1 database locally. (More on Prisma, migrations, and databases later.)Terminal window pnpm dev
Setting up TailwindCSS and shadcn/ui
Now, let’s install TailwindCSS and shadcn/ui.
TailwindCSS is a utility-first CSS framework that makes it easy to style your components.
shadcn/ui components is a library of pre-built components making it easier for us to focus on the functionality of our app, rather than the styling and building of components.
TailwindCSS
Since the RedwoodSDK is based on React and Vite, we can work through the “Using Vite” documentation.
-
Install Tailwind CSS
Terminal window pnpm install tailwindcss @tailwindcss/viteTerminal window npm install tailwindcss @tailwindcss/viteTerminal window yarn install tailwindcss @tailwindcss/vite -
Configure the Vite Plugin
vite.config.mts import { defineConfig } from "vite";import tailwindcss from '@tailwindcss/vite'import { redwood } from "@redwoodjs/sdk/vite";export default defineConfig({plugins: [redwood(),tailwindcss(),],}); -
Import Tailwind CSS. (You’ll need to create the
src/app/styles.css
file.)src/app/styles.css @import "tailwindcss"; -
Add the Tailwind CSS inside the
head
tag in theDocument.tsx
file.src/app/Document.tsx <head>...<link rel="stylesheet" href="/src/app/styles.css" />...</head> -
Now, you can run
pnpm run dev
and go tohttp://localhost:5173/user/login
.Terminal window pnpm run devFor reference, before adding TailwindCSS, the login page looked like this:
You can test this even further by going to the src/app/pages/user/Login.tsx
file and adding an h1
at the top of the return statement:
return ( <> <h1 className="text-4xl font-bold text-red-500">YOLO</h1>14 collapsed lines
<div ref={turnstile.ref} /> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" /> <button onClick={handlePerformPasskeyLogin} disabled={isPending}> {isPending ? <>...</> : "Login with passkey"} </button> <button onClick={handlePerformPasskeyRegister} disabled={isPending}> {isPending ? <>...</> : "Register with passkey"} </button> {result && <div>{result}</div>} </>);
Setting up Custom Fonts
First, let’s add the fonts that we’ll need for our project. We’re using Poppins and Inter, both can be found on Google Fonts.
From the font specimen page, click on the “Get Font” button.
Once both fonts have been added, click on the “View selected families” button on the top right. Then, click on the “Get Embed Code” button.
Next, let’s only select the font weights that we’ll need for our project.
Under Poppins, click on the “Change Styles” button. Turn everything off except for “500” and “700”.
Under Inter, if you click on the “Change Styles” button, you’ll see the settings are slightly different. That’s because this is variable font. A variable font is a single font file that contains multiple variations of a typeface, allowing for dynamic manipulation of the font. Meaning, nothing to do here.
Next, select the @import
radio button and copy the code.
Paste the code at the top of our styles.css
file. Then, remove the <style>
tags:
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@500;700&display=swap');@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
@import "tailwindcss";
Next, we need to add a custom configuration. In TailwindCSS v4, all customizations happen in the CSS file, not tailwind.config.js
.
Below the @import "tailwindcss";
line, add the following:
@theme { --font-display: "Poppins", sans-serif; --font-body: "Inter", sans-serif;}
If you’re unsure how the font-family is written, you can find it in the CSS class definition on the Google Fonts page. Here, Poppins and Inter are both in quotes and capitalized.
Now, you can use the class font-display
and font-body
in your project.
Setting up our Custom Color Palette
Defining colors is similar, except we’ll prepend each color value with --color-
.
@theme { ... --color-bg: #e4e3d4; --color-border: #eeeef0;
--color-primary: #f7b736; --color-secondary: #f1f1e8; --color-destructive: #ef533f;
--color-tag-applied: #b1c7c0; --color-tag-interview: #da9b7c; --color-tag-new: #db9a9f; --color-tag-rejected: #e4e3d4; --color-tag-offer: #aae198;}
We're using three different groups of colors:
- semantically named colors: background and border
- buttons: primary, secondary, and destructive
- tag colors: applied, interview, new, rejected, and offer
Now, we can use these colors for backgrounds (bg-bg
), borders (border-border
), and text (text-destructive
).
Full styles.css
file
styles.css
file@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@500;700&display=swap');@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
@import "tailwindcss";
@theme { --font-display: "Poppins", sans-serif; --font-body: "Inter", sans-serif;
--color-bg: #e4e3d4; --color-border: #eeeef0;
--color-primary: #f7b736; --color-secondary: #f1f1e8; --color-destructive: #ef533f;
--color-tag-applied: #b1c7c0; --color-tag-interview: #da9b7c; --color-tag-new: #db9a9f; --color-tag-rejected: #e4e3d4; --color-tag-offer: #aae198;}
Let’s test to make sure our custom colors are working. On the Login.tsx
file, let’s change the React fragment <></>
to a <main>
tag and add a className
of bg-bg
:
... return ( <main className="bg-bg">15 collapsed lines
<h1 className="text-4xl font-bold text-red-500">YOLO</h1> <div ref={turnstile.ref} /> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" /> <Button onClick={handlePerformPasskeyLogin} disabled={isPending}> {isPending ? <>...</> : "Login with passkey"} </Button> <Button onClick={handlePerformPasskeyRegister} disabled={isPending}> {isPending ? <>...</> : "Register with passkey"} </Button> {result && <div>{result}</div>} </main> );}
When you visit https://localhost:5173/user/login, you should see a beige background color:
shadcn/ui UI Components
You can also use the shadcn/ui Vite Installation instructions.
-
Install shadcn/ui
Terminal window pnpm dlx shadcn@latest initIt will ask you what theme you want to use. Let’s go with Neutral.
This command will create a
components.json
file in the root of your project. It contains all the configuration for our shadcn/ui components.Let’s modify the default paths so that it will put our components in the
src/app/components/ui
folder.components.json ..."aliases": {"components": "@/app/components","utils": "@/app/lib/utils","ui": "@/app/components/ui","lib": "@/app/lib","hooks": "@/app/hooks"}, -
Add path aliases to
tsconfig.json
:tsconfig.json {"compilerOptions": {"baseUrl": ".","paths": {"@/*": ["./src/*"]}}} -
Install
@types/node
Terminal window pnpm add -D @types/node -
Add resolve alias config to
vite.config.mts
:vite.config.mts import path from "path"import { defineConfig } from "vite";import tailwindcss from "@tailwindcss/vite"import { redwood } from "@redwoodjs/sdk/vite";export default defineConfig({plugins: [redwood(), tailwindcss()],resolve: {alias: {"@": path.resolve(__dirname, "./src"),},},) -
You should now be able to add components:
Terminal window pnpm dlx shadcn@latest addSelect the following by hitting the
Space
key:- Alert
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- Dialog
- Popover
- Select
- Sheet
- Sonner
- Table
When you’ve selected all the components you want to add, hit
Enter
. This will add all the components inside thesrc/app/components/ui
folder.This will install all of our components into the
src/app/components/ui
folder.
Even though we specified the path to the lib
directory in the components.json
file, the script still placed the folder inside the src
directory. You’ll need to move it into the app
directory.
Directorysrc/
Directoryapp/
Directorycomponents/
Directoryui/
- …
Directorylib/
- utils.ts
We also need the date picker component, but it can’t be installed using the pnpm dlx shadcn@latest add
command.
Instead, we’ll need to install it manually. It’s built using the <Popover />
and <Calendar />
components and we just installed those.
Within the src/app/components/ui
folder, create a new file called datepicker.tsx
.
Add the following code to the datepicker.tsx
file:
"use client"
import * as React from "react"import { format } from "date-fns"import { Calendar as CalendarIcon } from "lucide-react"
import { cn } from "@/app/lib/utils"import { Button } from "@/app/components/ui/button"import { Calendar } from "@/app/components/ui/calendar"import { Popover, PopoverContent, PopoverTrigger,} from "@/app/components/ui/popover"
export function DatePicker() { const [date, setDate] = React.useState<Date>()
return ( <Popover> <PopoverTrigger asChild> <Button variant={"outline"} className={cn( "w-[280px] justify-start text-left font-normal", !date && "text-muted-foreground" )} > <CalendarIcon className="mr-2 h-4 w-4" /> {date ? format(date, "PPP") : <span>Pick a date</span>} </Button> </PopoverTrigger> <PopoverContent className="w-auto p-0"> <Calendar mode="single" selected={date} onSelect={setDate} initialFocus /> </PopoverContent> </Popover> )}
- Official Date Picker documentation.
- If you copy and paste directly from the shadcn/ui documentation, though, you’ll find a few errors. In the code I provided, I adjusted the paths on some of the
@import
statements.
import { cn } from "@/lib/utils"import { cn } from "@/app/lib/utils"import { Button } from "@/components/ui/button"import { Button } from "@/app/components/ui/button"import { Calendar } from "@/components/ui/calendar"import { Calendar } from "@/app/components/ui/calendar"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"import { Popover, PopoverContent, PopoverTrigger,} from "@/app/components/ui/popover"
- I also changed the component name to
DatePicker
:
export function DatePickerDemo() {export function DatePicker() {
If you want to make sure everything is installed correctly, head over to the src/app/pages/user/Login.tsx
file and let’s use our Button
component.
Change the <button>
tag to use a capital B
. This will reference the Button
component, instead of the standard HTML button. You’ll also need to import the Button
component at the top of the file.
import { Button } from "@/app/components/ui/button";...return ( <>8 collapsed lines
<h1 className="text-4xl font-bold text-red-500">YOLO</h1> <div ref={turnstile.ref} /> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" /> <Button onClick={handlePerformPasskeyLogin} disabled={isPending}> {isPending ? <>...</> : "Login with passkey"} </Button> <Button onClick={handlePerformPasskeyRegister} disabled={isPending}> {isPending ? <>...</> : "Register with passkey"} </Button> {result && <div>{result}</div>} </>);
Now, when you visit https://localhost:5173/user/login, you should see styled buttons:
Perfect, now that we have our project setup, and all the frontend components installed, let’s start building the backend.