Skip to main content
Nuxt Secure is a universal Nuxt.js 4 application. The same codebase renders pages on both the server and the client. The Vue 3 frontend communicates exclusively with Nitro API routes running in the same process — there is no separate backend service.

Directory structure

Directory / filePurpose
app/Vue 3 frontend — pages, layouts, components, composables, middleware
app/middleware/Route guards that run before every navigation
app/composables/Shared reactive logic (useAuth, usePermisos, etc.)
server/Nitro server — API route handlers
server/api/REST-style endpoint files (.get.ts, .post.ts, etc.)
server/database/Drizzle ORM client and table schema
nuxt.config.tsFramework and module configuration

Data flow

Every request moves through this chain:
1. Browser
   └─ 2. auth.global.ts middleware (route guard)
         └─ 3. Vue 3 page / component
               └─ 4. fetch() → Nitro API route (server/api/)
                     └─ 5. Drizzle ORM query
                           └─ 6. Neon PostgreSQL (serverless)
The middleware runs on both the server (SSR) and the client (SPA navigation). useCookie is used instead of localStorage so token validation works in both environments.

Key files

FileDescription
app/middleware/auth.global.tsGlobal route guard — redirects unauthenticated users to /login
app/composables/useAuth.tsAuth state management — login flow, session restore, permission checks
server/api/All API route handlers (Nitro file-based routing)
server/database/schema.tsDrizzle ORM table definitions for all five tables
nuxt.config.tsNuxt modules, runtime config keys, Turnstile site key

nuxt.config.ts

nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  modules: ['@nuxtjs/tailwindcss', '@nuxtjs/turnstile'],

  nitro: {
    experimental: {
      openAPI: true
    }
  },

  turnstile: {
    siteKey: process.env.NUXT_PUBLIC_TURNSTILE_SITE_KEY,
  },

  runtimeConfig: {
    jwtSecret: process.env.JWT_SECRET,
    cloudinaryCloudName: process.env.CLOUDINARY_CLOUD_NAME,
    cloudinaryApiKey: process.env.CLOUDINARY_API_KEY,
    cloudinaryApiSecret: process.env.CLOUDINARY_API_SECRET,
    turnstile: {
      secretKey: process.env.TURNSTILE_SECRET_KEY,
    },
    public: {
      turnstileSiteKey: process.env.NUXT_PUBLIC_TURNSTILE_SITE_KEY
    }
  }
})

Database schema

Drizzle ORM manages five tables in a Neon PostgreSQL database. The relationships are: a usuario belongs to one perfil; a permisos_perfil row ties a perfil to a modulo with five boolean action flags; a menu row ties a menu item to a modulo.

perfil — user profiles / roles

ColumnTypeDescription
idserial PKAuto-increment primary key
strNombrePerfilvarchar(255)Profile display name
bitAdministradorbooleanWhether this profile has administrator privileges

usuario — application users

ColumnTypeDescription
idserial PKAuto-increment primary key
strNombreUsuariovarchar(255)Username used to log in
idPerfilinteger FKReference to perfil.id
strPwdvarchar(255)bcrypt-hashed password
idEstadoUsuariobooleantrue = active, false = inactive
strCorreovarchar(255) uniqueEmail address
strNumeroCelularvarchar(20)Optional mobile number
imagenUrlvarchar(500)Optional Cloudinary avatar URL

modulo — application modules

ColumnTypeDescription
idserial PKAuto-increment primary key
strNombreModulovarchar(255)Module name (used as the RBAC key)

permisos_perfil — permissions per profile per module

ColumnTypeDescription
idserial PKAuto-increment primary key
idModulointeger FKReference to modulo.id
idPerfilinteger FKReference to perfil.id
bitAgregarbooleanPermission to create records
bitEditarbooleanPermission to edit records
bitConsultabooleanPermission to view/list records
bitEliminarbooleanPermission to delete records
bitDetallebooleanPermission to view detail view
ColumnTypeDescription
idserial PKAuto-increment primary key
idMenuintegerMenu item identifier
idModulointeger FKReference to modulo.id

Full schema source

server/database/schema.ts
import { pgTable, serial, varchar, boolean, integer } from 'drizzle-orm/pg-core';

export const perfil = pgTable('perfil', {
  id: serial('id').primaryKey(),
  strNombrePerfil: varchar('strNombrePerfil', { length: 255 }).notNull(),
  bitAdministrador: boolean('bitAdministrador').notNull(),
});

export const usuario = pgTable('usuario', {
  id: serial('id').primaryKey(),
  strNombreUsuario: varchar('strNombreUsuario', { length: 255 }).notNull(),
  idPerfil: integer('idPerfil').references(() => perfil.id).notNull(),
  strPwd: varchar('strPwd', { length: 255 }).notNull(),
  idEstadoUsuario: boolean('idEstadoUsuario').default(true).notNull(),
  strCorreo: varchar('strCorreo', { length: 255 }).notNull().unique(),
  strNumeroCelular: varchar('strNumeroCelular', { length: 20 }),
  imagenUrl: varchar('imagenUrl', { length: 500 }),
});

export const modulo = pgTable('modulo', {
  id: serial('id').primaryKey(),
  strNombreModulo: varchar('strNombreModulo', { length: 255 }).notNull(),
});

export const permisosPerfil = pgTable('permisos_perfil', {
  id: serial('id').primaryKey(),
  idModulo: integer('idModulo').references(() => modulo.id).notNull(),
  idPerfil: integer('idPerfil').references(() => perfil.id).notNull(),
  bitAgregar: boolean('bitAgregar').default(false).notNull(),
  bitEditar: boolean('bitEditar').default(false).notNull(),
  bitConsulta: boolean('bitConsulta').default(false).notNull(),
  bitEliminar: boolean('bitEliminar').default(false).notNull(),
  bitDetalle: boolean('bitDetalle').default(false).notNull(),
});

export const menu = pgTable('menu', {
  id: serial('id').primaryKey(),
  idMenu: integer('idMenu').notNull(),
  idModulo: integer('idModulo').references(() => modulo.id).notNull(),
});

Build docs developers (and LLMs) love