Skip to content

Commit cb31009

Browse files
committed
feat: add Better Auth add-on for Solid
1 parent 8f7af71 commit cb31009

11 files changed

Lines changed: 389 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## Setting up Better Auth
2+
3+
1. Generate and set the `BETTER_AUTH_SECRET` environment variable in your `.env.local`:
4+
5+
```bash
6+
npx @better-auth/cli secret
7+
```
8+
9+
2. Visit the [Better Auth documentation](https://www.better-auth.com) to unlock the full potential of authentication in your app.
10+
11+
### Adding a Database (Optional)
12+
13+
Better Auth can work in stateless mode, but to persist user data, add a database:
14+
15+
```typescript
16+
// src/lib/auth.ts
17+
import { betterAuth } from "better-auth";
18+
import { Pool } from "pg";
19+
20+
export const auth = betterAuth({
21+
database: new Pool({
22+
connectionString: process.env.DATABASE_URL,
23+
}),
24+
// ... rest of config
25+
});
26+
```
27+
28+
Then run migrations:
29+
30+
```bash
31+
npx @better-auth/cli migrate
32+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Better Auth configuration
2+
BETTER_AUTH_URL=http://localhost:3000
3+
BETTER_AUTH_SECRET= # Generate a secret key: `npx @better-auth/cli secret`
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Show } from "solid-js";
2+
import { Link } from "@tanstack/solid-router";
3+
import { authClient } from "../../lib/auth-client";
4+
5+
export default function BetterAuthHeader() {
6+
const session = authClient.useSession();
7+
8+
return (
9+
<Show
10+
when={!session().isPending}
11+
fallback={
12+
<div class="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 animate-pulse" />
13+
}
14+
>
15+
<Show
16+
when={session().data?.user}
17+
fallback={
18+
<Link
19+
to="/demo/better-auth"
20+
class="h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors inline-flex items-center"
21+
>
22+
Sign in
23+
</Link>
24+
}
25+
>
26+
{(user) => (
27+
<div class="flex items-center gap-2">
28+
<Show
29+
when={user().image}
30+
fallback={
31+
<div class="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center">
32+
<span class="text-xs font-medium text-neutral-600 dark:text-neutral-400">
33+
{user().name?.charAt(0).toUpperCase() || "U"}
34+
</span>
35+
</div>
36+
}
37+
>
38+
{(image) => <img src={image()} alt="" class="h-8 w-8" />}
39+
</Show>
40+
<button
41+
onClick={() => authClient.signOut()}
42+
class="flex-1 h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors"
43+
>
44+
Sign out
45+
</button>
46+
</div>
47+
)}
48+
</Show>
49+
</Show>
50+
);
51+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createAuthClient } from 'better-auth/solid'
2+
3+
export const authClient = createAuthClient()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { betterAuth } from 'better-auth'
2+
3+
export const auth = betterAuth({
4+
emailAndPassword: {
5+
enabled: true,
6+
},
7+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createFileRoute } from '@tanstack/solid-router'
2+
import { auth } from '../../../lib/auth'
3+
4+
export const Route = createFileRoute('/api/auth/$')({
5+
server: {
6+
handlers: {
7+
GET: ({ request }) => auth.handler(request),
8+
POST: ({ request }) => auth.handler(request),
9+
},
10+
},
11+
})
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { createFileRoute } from "@tanstack/solid-router";
2+
import { createSignal, Show } from "solid-js";
3+
import { authClient } from "../lib/auth-client";
4+
5+
export const Route = createFileRoute("/demo/better-auth")({
6+
component: BetterAuthDemo,
7+
});
8+
9+
function BetterAuthDemo() {
10+
const session = authClient.useSession();
11+
const [isSignUp, setIsSignUp] = createSignal(false);
12+
const [email, setEmail] = createSignal("");
13+
const [password, setPassword] = createSignal("");
14+
const [name, setName] = createSignal("");
15+
const [error, setError] = createSignal("");
16+
const [loading, setLoading] = createSignal(false);
17+
18+
const handleSubmit = async (e: Event) => {
19+
e.preventDefault();
20+
setError("");
21+
setLoading(true);
22+
23+
try {
24+
if (isSignUp()) {
25+
const result = await authClient.signUp.email({
26+
email: email(),
27+
password: password(),
28+
name: name(),
29+
});
30+
if (result.error) {
31+
setError(result.error.message || "Sign up failed");
32+
}
33+
} else {
34+
const result = await authClient.signIn.email({
35+
email: email(),
36+
password: password(),
37+
});
38+
if (result.error) {
39+
setError(result.error.message || "Sign in failed");
40+
}
41+
}
42+
} catch (err) {
43+
setError("An unexpected error occurred");
44+
} finally {
45+
setLoading(false);
46+
}
47+
};
48+
49+
return (
50+
<Show
51+
when={!session().isPending}
52+
fallback={
53+
<div class="flex items-center justify-center py-10">
54+
<div class="h-5 w-5 animate-spin rounded-full border-2 border-neutral-200 border-t-neutral-900 dark:border-neutral-800 dark:border-t-neutral-100" />
55+
</div>
56+
}
57+
>
58+
<Show
59+
when={session().data?.user}
60+
fallback={
61+
<div class="flex justify-center py-10 px-4">
62+
<div class="w-full max-w-md p-6">
63+
<h1 class="text-lg font-semibold leading-none tracking-tight">
64+
{isSignUp() ? "Create an account" : "Sign in"}
65+
</h1>
66+
<p class="text-sm text-neutral-500 dark:text-neutral-400 mt-2 mb-6">
67+
{isSignUp()
68+
? "Enter your information to create an account"
69+
: "Enter your email below to login to your account"}
70+
</p>
71+
72+
<form onSubmit={handleSubmit} class="grid gap-4">
73+
<Show when={isSignUp()}>
74+
<div class="grid gap-2">
75+
<label for="name" class="text-sm font-medium leading-none">
76+
Name
77+
</label>
78+
<input
79+
id="name"
80+
type="text"
81+
value={name()}
82+
onInput={(e) => setName(e.currentTarget.value)}
83+
class="flex h-9 w-full border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 text-sm focus:outline-none focus:border-neutral-900 dark:focus:border-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
84+
required
85+
/>
86+
</div>
87+
</Show>
88+
89+
<div class="grid gap-2">
90+
<label for="email" class="text-sm font-medium leading-none">
91+
Email
92+
</label>
93+
<input
94+
id="email"
95+
type="email"
96+
value={email()}
97+
onInput={(e) => setEmail(e.currentTarget.value)}
98+
class="flex h-9 w-full border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 text-sm focus:outline-none focus:border-neutral-900 dark:focus:border-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
99+
required
100+
/>
101+
</div>
102+
103+
<div class="grid gap-2">
104+
<label
105+
for="password"
106+
class="text-sm font-medium leading-none"
107+
>
108+
Password
109+
</label>
110+
<input
111+
id="password"
112+
type="password"
113+
value={password()}
114+
onInput={(e) => setPassword(e.currentTarget.value)}
115+
class="flex h-9 w-full border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 text-sm focus:outline-none focus:border-neutral-900 dark:focus:border-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
116+
required
117+
minLength={8}
118+
/>
119+
</div>
120+
121+
<Show when={error()}>
122+
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3">
123+
<p class="text-sm text-red-600 dark:text-red-400">
124+
{error()}
125+
</p>
126+
</div>
127+
</Show>
128+
129+
<button
130+
type="submit"
131+
disabled={loading()}
132+
class="w-full h-9 px-4 text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-800 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
133+
>
134+
<Show
135+
when={!loading()}
136+
fallback={
137+
<span class="flex items-center justify-center gap-2">
138+
<span class="h-4 w-4 animate-spin rounded-full border-2 border-neutral-400 border-t-white dark:border-neutral-600 dark:border-t-neutral-900" />
139+
<span>Please wait</span>
140+
</span>
141+
}
142+
>
143+
{isSignUp() ? "Create account" : "Sign in"}
144+
</Show>
145+
</button>
146+
</form>
147+
148+
<div class="mt-4 text-center">
149+
<button
150+
type="button"
151+
onClick={() => {
152+
setIsSignUp(!isSignUp());
153+
setError("");
154+
}}
155+
class="text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 transition-colors"
156+
>
157+
{isSignUp()
158+
? "Already have an account? Sign in"
159+
: "Don't have an account? Sign up"}
160+
</button>
161+
</div>
162+
163+
<p class="mt-6 text-xs text-center text-neutral-400 dark:text-neutral-500">
164+
Built with{" "}
165+
<a
166+
href="https://better-auth.com"
167+
target="_blank"
168+
rel="noopener noreferrer"
169+
class="font-medium hover:text-neutral-600 dark:hover:text-neutral-300"
170+
>
171+
BETTER-AUTH
172+
</a>
173+
.
174+
</p>
175+
</div>
176+
</div>
177+
}
178+
>
179+
{(user) => (
180+
<div class="flex justify-center py-10 px-4">
181+
<div class="w-full max-w-md p-6 space-y-6">
182+
<div class="space-y-1.5">
183+
<h1 class="text-lg font-semibold leading-none tracking-tight">
184+
Welcome back
185+
</h1>
186+
<p class="text-sm text-neutral-500 dark:text-neutral-400">
187+
You're signed in as {user().email}
188+
</p>
189+
</div>
190+
191+
<div class="flex items-center gap-3">
192+
<Show
193+
when={user().image}
194+
fallback={
195+
<div class="h-10 w-10 bg-neutral-200 dark:bg-neutral-800 flex items-center justify-center">
196+
<span class="text-sm font-medium text-neutral-600 dark:text-neutral-400">
197+
{user().name?.charAt(0).toUpperCase() || "U"}
198+
</span>
199+
</div>
200+
}
201+
>
202+
{(image) => <img src={image()} alt="" class="h-10 w-10" />}
203+
</Show>
204+
<div class="flex-1 min-w-0">
205+
<p class="text-sm font-medium truncate">{user().name}</p>
206+
<p class="text-xs text-neutral-500 dark:text-neutral-400 truncate">
207+
{user().email}
208+
</p>
209+
</div>
210+
</div>
211+
212+
<button
213+
onClick={() => authClient.signOut()}
214+
class="w-full h-9 px-4 text-sm font-medium border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
215+
>
216+
Sign out
217+
</button>
218+
219+
<p class="text-xs text-center text-neutral-400 dark:text-neutral-500">
220+
Built with{" "}
221+
<a
222+
href="https://better-auth.com"
223+
target="_blank"
224+
rel="noopener noreferrer"
225+
class="font-medium hover:text-neutral-600 dark:hover:text-neutral-300"
226+
>
227+
BETTER-AUTH
228+
</a>
229+
.
230+
</p>
231+
</div>
232+
</div>
233+
)}
234+
</Show>
235+
</Show>
236+
);
237+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "Better Auth",
3+
"description": "Add Better Auth authentication to your application.",
4+
"phase": "add-on",
5+
"type": "add-on",
6+
"link": "https://www.better-auth.com/",
7+
"modes": ["file-router"],
8+
"dependsOn": ["start"],
9+
"routes": [
10+
{
11+
"url": "/demo/better-auth",
12+
"name": "Better Auth",
13+
"path": "src/routes/demo.better-auth.tsx",
14+
"jsName": "BetterAuthDemo"
15+
}
16+
],
17+
"integrations": [
18+
{
19+
"type": "header-user",
20+
"jsName": "BetterAuthHeader",
21+
"path": "src/integrations/better-auth/header-user.tsx"
22+
}
23+
]
24+
}
Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"better-auth": "^1.4.12"
4+
}
5+
}

0 commit comments

Comments
 (0)