├── .gitignore ├── README.md ├── app.vue ├── assets └── main.css ├── components ├── Account.vue ├── Auth.vue └── Avatar.vue ├── nuxt.config.ts ├── package-lock.json ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | 10 | # vscode 11 | .history 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Supabase Nuxt User Management 2 | 3 | > **Warning** 4 | > All example apps have been consolidated to Supabase's main repo. You can find the nuxt3 quick start guide [here](https://github.com/supabase/supabase/tree/master/examples/user-management/nuxt3-user-management). 5 | 6 | This repo is a quick sample of how you can get started building apps using Nuxt 3 and Supabase. You can find a step by step guide of how to build out this app in the [Quickstart: Nuxt guide](https://supabase.io/docs/guides/with-nuxt-3). 7 | 8 | This repo will demonstrate how to: 9 | - sign users in with Supabase Auth using [magic link](https://supabase.io/docs/reference/dart/auth-signin#sign-in-with-magic-link) 10 | - store and retrieve data with [Supabase database](https://supabase.io/docs/guides/database) 11 | - store image files in [Supabase storage](https://supabase.io/docs/guides/storage) 12 | 13 | ## Getting Started 14 | 15 | Before running this app, you need to create a Supabase project and copy [your credentials](https://supabase.io/docs/guides/with-nuxt-3#get-the-api-keys) to `.env`. 16 | 17 | Run the following command to launch it on `localhost:3000` 18 | ```bash 19 | npm run dev 20 | ``` 21 | 22 | ## Database Schema 23 | 24 | ```sql 25 | -- Create a table for public "profiles" 26 | create table profiles ( 27 | id uuid references auth.users not null, 28 | updated_at timestamp with time zone, 29 | username text unique, 30 | avatar_url text, 31 | website text, 32 | 33 | primary key (id), 34 | unique(username), 35 | constraint username_length check (char_length(username) >= 3) 36 | ); 37 | 38 | alter table profiles enable row level security; 39 | 40 | create policy "Public profiles are viewable by everyone." 41 | on profiles for select 42 | using ( true ); 43 | 44 | create policy "Users can insert their own profile." 45 | on profiles for insert 46 | with check ( auth.uid() = id ); 47 | 48 | create policy "Users can update own profile." 49 | on profiles for update 50 | using ( auth.uid() = id ); 51 | 52 | -- Set up Realtime! 53 | begin; 54 | drop publication if exists supabase_realtime; 55 | create publication supabase_realtime; 56 | commit; 57 | alter publication supabase_realtime add table profiles; 58 | 59 | -- Set up Storage! 60 | insert into storage.buckets (id, name) 61 | values ('avatars', 'avatars'); 62 | 63 | create policy "Avatar images are publicly accessible." 64 | on storage.objects for select 65 | using ( bucket_id = 'avatars' ); 66 | 67 | create policy "Anyone can upload an avatar." 68 | on storage.objects for insert 69 | with check ( bucket_id = 'avatars' ); 70 | ``` 71 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | --custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 4 | Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 5 | --custom-bg-color: #101010; 6 | --custom-panel-color: #222; 7 | --custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8); 8 | --custom-color: #fff; 9 | --custom-color-brand: #24b47e; 10 | --custom-color-secondary: #666; 11 | --custom-border: 1px solid #333; 12 | --custom-border-radius: 5px; 13 | --custom-spacing: 5px; 14 | 15 | padding: 0; 16 | margin: 0; 17 | font-family: var(--custom-font-family); 18 | background-color: var(--custom-bg-color); 19 | } 20 | 21 | * { 22 | color: var(--custom-color); 23 | font-family: var(--custom-font-family); 24 | box-sizing: border-box; 25 | } 26 | 27 | html, 28 | body, 29 | #__next { 30 | height: 100vh; 31 | width: 100vw; 32 | overflow-x: hidden; 33 | } 34 | 35 | /* Grid */ 36 | 37 | .container { 38 | width: 90%; 39 | margin-left: auto; 40 | margin-right: auto; 41 | } 42 | 43 | .row { 44 | position: relative; 45 | width: 100%; 46 | } 47 | 48 | .row [class^="col"] { 49 | float: left; 50 | margin: 0.5rem 2%; 51 | min-height: 0.125rem; 52 | } 53 | 54 | .col-1, 55 | .col-2, 56 | .col-3, 57 | .col-4, 58 | .col-5, 59 | .col-6, 60 | .col-7, 61 | .col-8, 62 | .col-9, 63 | .col-10, 64 | .col-11, 65 | .col-12 { 66 | width: 96%; 67 | } 68 | 69 | .col-1-sm { 70 | width: 4.33%; 71 | } 72 | 73 | .col-2-sm { 74 | width: 12.66%; 75 | } 76 | 77 | .col-3-sm { 78 | width: 21%; 79 | } 80 | 81 | .col-4-sm { 82 | width: 29.33%; 83 | } 84 | 85 | .col-5-sm { 86 | width: 37.66%; 87 | } 88 | 89 | .col-6-sm { 90 | width: 46%; 91 | } 92 | 93 | .col-7-sm { 94 | width: 54.33%; 95 | } 96 | 97 | .col-8-sm { 98 | width: 62.66%; 99 | } 100 | 101 | .col-9-sm { 102 | width: 71%; 103 | } 104 | 105 | .col-10-sm { 106 | width: 79.33%; 107 | } 108 | 109 | .col-11-sm { 110 | width: 87.66%; 111 | } 112 | 113 | .col-12-sm { 114 | width: 96%; 115 | } 116 | 117 | .row::after { 118 | content: ""; 119 | display: table; 120 | clear: both; 121 | } 122 | 123 | .hidden-sm { 124 | display: none; 125 | } 126 | 127 | @media only screen and (min-width: 33.75em) { 128 | 129 | /* 540px */ 130 | .container { 131 | width: 80%; 132 | } 133 | } 134 | 135 | @media only screen and (min-width: 45em) { 136 | 137 | /* 720px */ 138 | .col-1 { 139 | width: 4.33%; 140 | } 141 | 142 | .col-2 { 143 | width: 12.66%; 144 | } 145 | 146 | .col-3 { 147 | width: 21%; 148 | } 149 | 150 | .col-4 { 151 | width: 29.33%; 152 | } 153 | 154 | .col-5 { 155 | width: 37.66%; 156 | } 157 | 158 | .col-6 { 159 | width: 46%; 160 | } 161 | 162 | .col-7 { 163 | width: 54.33%; 164 | } 165 | 166 | .col-8 { 167 | width: 62.66%; 168 | } 169 | 170 | .col-9 { 171 | width: 71%; 172 | } 173 | 174 | .col-10 { 175 | width: 79.33%; 176 | } 177 | 178 | .col-11 { 179 | width: 87.66%; 180 | } 181 | 182 | .col-12 { 183 | width: 96%; 184 | } 185 | 186 | .hidden-sm { 187 | display: block; 188 | } 189 | } 190 | 191 | @media only screen and (min-width: 60em) { 192 | 193 | /* 960px */ 194 | .container { 195 | width: 75%; 196 | max-width: 60rem; 197 | } 198 | } 199 | 200 | /* Forms */ 201 | 202 | label { 203 | display: block; 204 | margin: 5px 0; 205 | color: var(--custom-color-secondary); 206 | font-size: 0.8rem; 207 | text-transform: uppercase; 208 | } 209 | 210 | input { 211 | width: 100%; 212 | border-radius: 5px; 213 | border: var(--custom-border); 214 | padding: 8px; 215 | font-size: 0.9rem; 216 | background-color: var(--custom-bg-color); 217 | color: var(--custom-color); 218 | } 219 | 220 | input[disabled] { 221 | color: var(--custom-color-secondary); 222 | } 223 | 224 | /* Utils */ 225 | 226 | .block { 227 | display: block; 228 | width: 100%; 229 | } 230 | 231 | .inline-block { 232 | display: inline-block; 233 | width: 100%; 234 | } 235 | 236 | .flex { 237 | display: flex; 238 | } 239 | 240 | .flex.column { 241 | flex-direction: column; 242 | } 243 | 244 | .flex.row { 245 | flex-direction: row; 246 | } 247 | 248 | .flex.flex-1 { 249 | flex: 1 1 0; 250 | } 251 | 252 | .flex-end { 253 | justify-content: flex-end; 254 | } 255 | 256 | .flex-center { 257 | justify-content: center; 258 | } 259 | 260 | .items-center { 261 | align-items: center; 262 | } 263 | 264 | .text-sm { 265 | font-size: 0.8rem; 266 | font-weight: 300; 267 | } 268 | 269 | .text-right { 270 | text-align: right; 271 | } 272 | 273 | .font-light { 274 | font-weight: 300; 275 | } 276 | 277 | .opacity-half { 278 | opacity: 50%; 279 | } 280 | 281 | /* Button */ 282 | 283 | button, 284 | .button { 285 | color: var(--custom-color); 286 | border: var(--custom-border); 287 | background-color: var(--custom-bg-color); 288 | display: inline-block; 289 | text-align: center; 290 | border-radius: var(--custom-border-radius); 291 | padding: 0.5rem 1rem; 292 | cursor: pointer; 293 | text-align: center; 294 | font-size: 0.9rem; 295 | text-transform: uppercase; 296 | } 297 | 298 | button.primary, 299 | .button.primary { 300 | background-color: var(--custom-color-brand); 301 | border: 1px solid var(--custom-color-brand); 302 | } 303 | 304 | /* Widgets */ 305 | 306 | .card { 307 | width: 100%; 308 | display: block; 309 | border: var(--custom-border); 310 | border-radius: var(--custom-border-radius); 311 | padding: var(--custom-spacing); 312 | } 313 | 314 | .avatar { 315 | border-radius: var(--custom-border-radius); 316 | overflow: hidden; 317 | max-width: 100%; 318 | } 319 | 320 | .avatar.image { 321 | object-fit: cover; 322 | } 323 | 324 | .avatar.no-image { 325 | background-color: #333; 326 | border: 1px solid rgb(200, 200, 200); 327 | border-radius: 5px; 328 | } 329 | 330 | .footer { 331 | position: absolute; 332 | max-width: 100%; 333 | bottom: 0; 334 | left: 0; 335 | right: 0; 336 | display: flex; 337 | flex-flow: row; 338 | border-top: var(--custom-border); 339 | background-color: var(--custom-bg-color); 340 | } 341 | 342 | .footer div { 343 | padding: var(--custom-spacing); 344 | display: flex; 345 | align-items: center; 346 | width: 100%; 347 | } 348 | 349 | .footer div>img { 350 | height: 20px; 351 | margin-left: 10px; 352 | } 353 | 354 | .footer>div:first-child { 355 | display: none; 356 | } 357 | 358 | .footer>div:nth-child(2) { 359 | justify-content: left; 360 | } 361 | 362 | @media only screen and (min-width: 60em) { 363 | 364 | /* 960px */ 365 | .footer>div:first-child { 366 | display: flex; 367 | } 368 | 369 | .footer>div:nth-child(2) { 370 | justify-content: center; 371 | } 372 | } 373 | 374 | @keyframes spin { 375 | from { 376 | transform: rotate(0deg); 377 | } 378 | 379 | to { 380 | transform: rotate(360deg); 381 | } 382 | } 383 | 384 | .mainHeader { 385 | width: 100%; 386 | font-size: 1.3rem; 387 | margin-bottom: 20px; 388 | } 389 | 390 | .avatarPlaceholder { 391 | border: var(--custom-border); 392 | border-radius: var(--custom-border-radius); 393 | width: 35px; 394 | height: 35px; 395 | background-color: rgba(255, 255, 255, 0.2); 396 | display: flex; 397 | align-items: center; 398 | justify-content: center; 399 | } 400 | 401 | .form-widget { 402 | display: flex; 403 | flex-direction: column; 404 | gap: 20px; 405 | } 406 | 407 | .form-widget>.button { 408 | display: flex; 409 | align-items: center; 410 | justify-content: center; 411 | border: none; 412 | background-color: #444444; 413 | text-transform: none !important; 414 | transition: all 0.2s ease; 415 | } 416 | 417 | .form-widget .button:hover { 418 | background-color: #2a2a2a; 419 | } 420 | 421 | .form-widget .button>.loader { 422 | width: 17px; 423 | animation: spin 1s linear infinite; 424 | filter: invert(1); 425 | } -------------------------------------------------------------------------------- /components/Account.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 88 | -------------------------------------------------------------------------------- /components/Auth.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | -------------------------------------------------------------------------------- /components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt' 2 | 3 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 4 | export default defineNuxtConfig({ 5 | modules: ['@nuxtjs/supabase'], 6 | css: ['@/assets/main.css'], 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview" 8 | }, 9 | "devDependencies": { 10 | "@nuxtjs/supabase": "^0.1.23", 11 | "nuxt": "3.0.0-rc.8" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------