Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Getting started

Starter kits

Every new Oriel app starts from a starter kit: sign-in pages, settings and a dashboard in React, Vue, Svelte, Solid, Angular or Live, or a JSON API with tokens.

Introduction#

A starter kit gives your new app the routes, controllers, pages and tests it needs to register and sign in its users. The code is copied into your app when you create it, so it is yours: change any of it, or delete what you don't need. There is nothing to keep in sync with the kit afterwards.

The kits sign users in with Oriel's own authentication, without a separate package such as Laravel's Fortify: the controllers, or the Live components, are in your app.

Creating an application using a starter kit#

The installer asks which kit you want, or takes it as an option (see Installation):

Terminal
npm create @orieljs@latest blog -- --inertia=reactnpm create @orieljs@latest blog -- --livenpm create @orieljs@latest blog -- --api

It installs the app, sets its key, migrates the database and builds the assets, so the app runs straight away. While you work on it, run Vite and the server in two terminals:

Terminal
npm run dev
Terminal
node oriel serve --watch

Then open http://localhost:8000. serve --watch restarts the server when your server code changes, and Vite updates your pages' scripts and styles in the browser. After a restart, reload the page once more: Vite doesn't reload it once the new server is up (see Running your application).

A new app sends no real mail: MAIL_MAILER=log in .env writes each mail to storage/logs/oriel.log, so the email verification link and the password reset link are in there. Set the MAIL_* variables to send real mail.

Available starter kits#

React#

The React kit's pages are React 19 components, served through Inertia by @orieljs/inertia-react and rendered on the server. They are written in TypeScript and styled with Tailwind CSS 4 and the Oriel theme.

Vue#

The Vue kit's pages are Vue 3 single-file components with <script setup lang="ts">, served through Inertia by @orieljs/inertia-vue and rendered on the server.

Svelte#

The Svelte kit's pages are Svelte 5 components, written with runes, served through Inertia by @orieljs/inertia-svelte and rendered on the server.

Solid#

The Solid kit's pages are Solid components, served through Inertia by @orieljs/inertia-solid and rendered on the server.

Angular#

The Angular kit's pages are standalone Angular 21 components, served through Inertia by @orieljs/inertia-angular. A page receives its props as signal inputs. The Angular kit renders its pages in the browser only: it has no server-side rendering.

Live#

The Live kit's pages are Live components: TSX classes rendered on the server, whose public fields are their state and whose public methods are the actions the browser can call. @orieljs/live-client keeps them interactive in the browser. Live is Oriel's Livewire.

API#

The API kit has no pages: JSON in and out, personal access tokens, and no sessions, cookies or views. Its endpoints are listed in The API kit.

Starter kit customization#

The Inertia kits#

The five Inertia kits share their server code: the same controllers, routes and form requests. Only resources/js differs:

FolderWhat is in it
resources/js/pagesThe pages: Auth/Login, Auth/Register, Auth/ForgotPassword, Auth/ResetPassword, Auth/VerifyEmail, Dashboard, Settings/Profile, Settings/Password, Settings/Appearance
resources/js/layoutsAppLayout (the sidebar and the top bar), AuthLayout (the guest pages) and SettingsLayout (the settings tabs)
resources/js/componentsNavLink, SidebarNav, UserMenu, AppearanceSwitch, Icon, Brand, CopyButton, InputError and DeleteUser, with a few more per framework, such as React's Field and Status
resources/js/app.*, resources/js/ssr.*The browser entry, and the server entry of the kits with server-side rendering
resources/js/appearance.ts, checklist.ts, menus.ts, names.tsThe browser code every kit shares

Each framework keeps the Light / Dark / System setting in its own idiom: React's hooks/useAppearance.ts, Vue's composables/useAppearance.ts, Svelte's lib/appearance.svelte.ts, Solid's lib/appearance.ts and Angular's services/appearance.service.ts. The Angular kit also has PageTitleService, which sets the document's title and the breadcrumb.

resources/views/app.tsx is the root view every page is rendered into. HandleInertiaRequests, in app/Http/Middleware, shares the validation errors and three props with every page: name, the app's name; auth.user, the signed-in user's id, name, email and email_verified_at, or null; and appearance.

A page's props are typed. node oriel inertia:types writes resources/js/oriel/pages.d.ts from your controllers, and node oriel route:types writes the route table behind route('dashboard') in your pages. The kits' Vite plugin runs both when Vite starts, and again when your server code changes while npm run dev runs, and npm run typecheck runs them before TypeScript. Inertia explains them.

The Live kit#

The Live kit's pages are components in app/Live, and its other markup is in resources/views:

PathWhat is in it
app/Live/AuthThe guest pages: Login, Register, ForgotPassword and ResetPassword, and VerifyEmail
app/Live/SettingsProfile, with DeleteUserForm inside it, and Password
app/Live/Actions/Logout.tsxThe user menu's Log out button, and the sign-out the other components share
resources/views/dashboard.tsx, resources/views/settings/appearance.tsxThe dashboard and Settings → Appearance, as plain views
resources/views/components/layoutsapp (the sidebar and the top bar) and auth (the guest pages), the documents the pages render into
resources/views/componentsauth-page, settings/layout, field, status, nav-link, text-link, sidebar-nav, user-menu, appearance-switch, icon, brand and copy-button
resources/js/app.tsStarts Live in the browser, with the Light / Dark / System switch, the menus, and the dashboard's Copy buttons and tick

A Live page's route names its component: Route.get('login', Login). Every update the browser sends runs the page route's auth, verified, guest and can middleware again, so a signed-out tab can't run an action. TypeScript's private doesn't hide a method from the browser: name it with # or a leading _, or mark it @Locked. Live explains components.

The dashboard#

The dashboard greets the user by first name. In the local environment, it also shows a Getting started checklist and the app's details; elsewhere it shows neither. The checklist ticks its steps from your app's real state:

StepDone when
Create your accountAlways: you are signed in
Verify your emailYour email address is verified
Add your first pageYour app has a named route that isn't one of the kit's or the framework's
Store some dataapp/Models has a model besides User
Run the testsYou tick it: the browser remembers it in localStorage

The "Your app" card shows the environment, the Oriel version, the runtime, the database, the queue connection, the mailer, and whether the health route's checks pass. app/Support/GettingStarted.ts builds both; change it, or remove the cards from the dashboard page, as you like.

Two steps have a command with a Copy button. A click copies the command and the button says "Copied" for two seconds. A polite live region after the button says it too, so a screen reader announces it. Where the browser offers no clipboard, as on plain http away from localhost, the button stays on "Copy".

Appearance#

Settings → Appearance offers Light, Dark and System, and new users start on System. The choice is the appearance cookie, kept for a year and mirrored to localStorage. The browser writes it, so bootstrap/app.ts leaves it out of cookie encryption: EncryptCookies.except('appearance').

The HandleAppearance middleware reads the cookie for each request, so the server renders the page in the right theme: the root view puts the dark class on <html> for Dark. For System, the head carries a short inline script, before the stylesheet, that adds dark before the first paint when the system is dark, and the browser code follows the system setting as it changes. The script carries the request's CSP nonce when your app sets one with vite().useCspNonce(), as the tags vite() writes do, so a strict Content Security Policy doesn't block it.

The welcome page, at /, follows the same cookie. Its styles are inline, with the nonce, and use plain custom properties rather than CSS's light-dark(), so browsers without light-dark(), such as Safari before 17.5 and Chrome before 123, get both themes too.

The stylesheet#

The kits with pages share resources/css/app.css: Tailwind CSS 4 with the Oriel theme, one violet accent and system fonts, in light and dark. Tailwind's dark: variant follows the dark class on <html>. The theme adds:

  • Colours that follow the theme: text-ink, text-body, text-muted, text-faint, border-line, text-accent, text-success and text-danger (each with every utility Tailwind makes from a colour), and Oriel's fixed colours, such as bg-midnight-canvas and text-glow-violet.
  • A type scale in place of Tailwind's: text-small, text-sm, text-body, text-body-lg, text-intro, text-subheading, text-heading-sm, text-heading, text-heading-lg and text-display.
  • Radii in place of Tailwind's: rounded-control, rounded-list, rounded-glass and rounded-pill. Tailwind's shadow scales are removed: the component classes carry the depth.

And the component classes the pages are built from:

GroupClasses
Buttonsbtn (the primary), btn-secondary, btn-quiet, btn-danger, btn-ghost, btn-sm, btn-icon
Surfacescard, card-danger, glass
Texttitle, section-title, description, link, code
Formsform, field, label, input, checkbox, error, status
Navigationnav-link, seg, seg-icons, crumbs, menu, menu-button, menu-panel, menu-panel-start, menu-who, menu-item
Layoutshell, sidebar, side-nav, side-nav-end, shell-main, topbar, page, section, sky, sky-header, brand, avatar
Dashboardprogress, step, step-title, step-text, check, copy, copy-label, facts, healthy
Appearancepreviews, preview, preview-swatch, preview-light, preview-dark, preview-system

Utilities still override them: <button class="btn btn-sm w-full">.

Accessibility#

The kits are built to be read by everyone:

  • Every text colour, --faint included (placeholders, the breadcrumb's separator, finished checklist steps), keeps a contrast of 4.5:1 (WCAG AA) on the page, cards, the top bar, the sidebar and inputs, in both themes. So do the welcome page's footer and step numbers.
  • .btn-danger, the Delete account button, is dark enough for its white label to keep 4.5:1, hovered or not.
  • .status stays in the page even when it is empty, so a screen reader announces the status text that appears in it.
  • The Copy buttons announce "Copied" through a polite live region.
  • The menus are native <details> elements: Escape closes one and returns the focus to its button, and a click elsewhere, a choice inside it or the focus moving out of it closes it too.
  • :focus-visible draws a violet outline around the focused control.

Authentication#

The kits with pages register their routes in routes/auth.ts and routes/settings.ts, and the dashboard and the welcome page in routes/web.ts:

URIMethodRoute nameWhat it does
/GEThomeThe welcome page
/dashboardGETdashboardThe dashboard, for verified users
/registerGET, POSTregisterThe registration page, and registering
/loginGET, POSTloginThe login page, and signing in
/logoutPOSTlogoutSigning out
/forgot-passwordGET, POSTpassword.request, password.emailThe reset link page, and sending the link
/reset-password/{token}GETpassword.resetThe new password page
/reset-passwordPOSTpassword.storeSetting the new password
/verify-emailGETverification.noticeThe page that asks the user to verify their email
/verify-email/{id}/{hash}GETverification.verifyThe signed link in the verification mail
/email/verification-notificationPOSTverification.sendSending the verification mail again
/settingsGETA redirect to /settings/profile
/settings/profileGET, PATCH, DELETEprofile.edit, profile.update, profile.destroyThe profile page, updating the name and email, and deleting the account
/settings/passwordGET, PUTpassword.edit, password.updateThe password page, and changing the password
/settings/appearanceGETappearance.editThe appearance page

Where a URI has two methods and one name, the name is the GET route's. The guest pages are behind the guest middleware, and the rest behind auth. In the Live kit, a page's route is a GET route to its component, and the forms are the component's actions: the POST, PATCH, PUT and DELETE routes, and logout and verification.send, aren't there. node oriel route:list shows them all.

Email verification#

Users verify their email address before they reach the dashboard: its route has the verified middleware. A new user gets the verification mail when they register, and an unverified user is sent to /verify-email, which can send it again. A user who changes their email address on the profile page has to verify the new one.

To let users in without verifying, take verified off the dashboard's route:

routes/web.ts
import { Route, view } from '@orieljs/framework'import { DashboardController } from '../app/Http/Controllers/DashboardController'Route.get('/', () => view('welcome')).name('home')Route.middleware(['auth']).group(() => {  Route.get('dashboard', DashboardController).name('dashboard')})

In the Live kit, take it off the Route.view('dashboard', 'dashboard') route the same way. The verification mail is still sent when a user registers.

Rate limiting#

The kits limit the routes an attacker would try many times:

WhatLimit
Signing in5 failed attempts per email address and IP address, then locked for a minute
The verification link, and sending it again6 requests a minute (throttle:6,1)
Changing the password6 requests a minute (throttle:6,1)
Sending a reset linkOne link per email address a minute (passwords.users.throttle in config/auth.ts)

The login limit is in LoginRequest, in app/Http/Requests/Auth (in the Live kit, in the Login component): change the 5 in tooManyAttempts() to allow more. In the Live kit, sending the verification mail again and changing the password are component actions, which have no limit of their own.

Customizing user creation#

Registration is RegisteredUserController's store() (in the Live kit, the Register component's register()). It validates the form, creates the user, fires the Registered event, which sends the verification mail, and signs the user in. To ask for more, add the field's rule and save it, after a migration that adds its column:

app/Http/Controllers/Auth/RegisteredUserController.ts
import { Auth, event, Hash, inertia, redirect, Registered, request, Rule } from '@orieljs/framework'import { User } from '../../../Models/User'import { Controller } from '../Controller'export class RegisteredUserController extends Controller {  /** Show the registration page. */  create() {    return inertia('Auth/Register')  }  /** Handle an incoming registration request. */  async store() {    const data = await request().validate({      name: 'required|string|max:255',      email: 'required|string|lowercase|email|max:255|unique:users',      phone: 'required|string|max:20',      password: ['required', 'confirmed', Rule.password()],    })    const user = await User.create({      name: data.name,      email: data.email,      phone: data.phone,      password: await Hash.make(data.password),    })    await event(new Registered(user))    await Auth.login(user)    return redirect().route('dashboard')  }}

Add phone to the model's fillable too, and the field to the registration page. Rule.password() asks for at least 8 characters; Validating passwords shows how to ask for more.

The password reset is NewPasswordController (the ResetPassword component in Live), and the profile is ProfileController and ProfileUpdateRequest (the Profile component). Deleting an account asks for the current password, then signs the user out and deletes them.

The API kit#

The API kit's routes are in routes/api.ts, so their URIs start with /api:

URIMethodRoute nameTokenAnswer
/api/registerPOSTregister201 with { user, token }
/api/loginPOSTlogin{ token }; a 422 on bad credentials, locked for a minute after five failures
/api/userGETuserYesThe user
/api/logoutPOSTlogoutYes204; the token is revoked
/api/forgot-passwordPOSTpassword.email{ status }, the same whether or not the account exists
/api/reset-passwordPOSTpassword.store{ status }; every token of the user is revoked

Send the token in the Authorization header: Authorization: Bearer <token>. The routes with a token are behind auth:token, and the four without one are limited to 60 requests a minute per client (throttle:60,1). Errors are JSON under /api, whatever the request's Accept header: bootstrap/app.ts says so with shouldRenderJsonWhen().

The reset mail links to /reset-password/{token}?email=… on the host the request came to, which is your API's own address. That page belongs to your client app, which posts the token, the email and the new password to /api/reset-password, so point the link at it with ResetPassword.createUrlUsing() (see Reset link customization).

The API kit has no email verification. To add it, add the verification routes and fire the Registered event in RegisteredUserController, which then sends the mail.

Inertia SSR#

The React, Vue, Svelte and Solid kits render their pages on the server, so the first visit gets HTML. The Angular kit renders its pages in the browser only.

npm run build builds both bundles: the browser's into public/build, and the server's, from resources/js/ssr.*, into bootstrap/ssr. While npm run dev runs, Vite renders the pages on the server instead; right after it starts, the first pages can come back rendered in the browser only while it warms up, and Vite prints "SSR skipped".

In production, run the SSR server next to your app:

Terminal
node oriel inertia:start-ssr

On Bun, run bun oriel inertia:start-ssr --runtime=bun. The server listens on the port of INERTIA_SSR_URL, http://127.0.0.1:13714 unless you set it, and INERTIA_SSR_ENABLED=false turns server-side rendering off. When no SSR server answers, the pages are rendered in the browser, as in the Angular kit. inertia:check-ssr tells you whether it is running, and inertia:stop-ssr stops it. Deployment shows how to keep it running.

Frequently asked questions#

How do I upgrade?#

The kit's code is your app's from the start, so there is no kit to upgrade. The framework itself is a dependency: update @orieljs/framework and the other @orieljs/* packages with your package manager.

Where is the mail a new user receives?#

With the log mailer, a new app's default, in storage/logs/oriel.log, or storage/logs/mail.log with MAIL_LOG_CHANNEL=mail in .env. The verification mail and the reset mail are Oriel's own notifications; the Notifications page shows how to change them.

Privacy policy ·

esc