Overview
This guide covers installation and configuration for both development and production environments. ICL Cotizaciones is a Next.js 16 application using SQLite as its database, making it straightforward to deploy as a single-server application.
For a quick local setup, see the Quickstart guide. This guide covers production-ready deployment.
System Requirements
Hardware
CPU : 2+ cores recommended
RAM : 2GB minimum, 4GB recommended
Storage : 10GB minimum (database grows with quotation volume)
Network : Static IP or domain name for production
Software
Node.js : 18.0 or higher (20.x LTS recommended)
npm : 9.0 or higher
Build Tools : Required for native module compilation
macOS: Xcode Command Line Tools
Linux: build-essential, python3
Windows: Visual Studio Build Tools
Clone Repository
Get the source code: git clone < repository-ur l > iclapp
cd iclapp
Install Dependencies
Install all required npm packages: Key Dependencies: package.json - Framework & Core
package.json - UI Components
package.json - Styling
{
"dependencies" : {
"next" : "^16.1.6" ,
"react" : "^19.2.4" ,
"react-dom" : "^19.2.4" ,
"drizzle-orm" : "^0.45.1" ,
"better-sqlite3" : "^12.6.2" ,
"iron-session" : "^8.0.4" ,
"bcryptjs" : "^3.0.3"
}
}
better-sqlite3 is a native Node.js addon that requires compilation during install. Ensure build tools are available.
Configure Environment Variables
Create a .env.local file in the project root: # Session Configuration
SESSION_SECRET = "your-secret-key-at-least-32-characters-long-change-this-in-production"
SESSION_SECURE = false # Set to true in production with HTTPS
# Database Configuration
DATABASE_PATH = "./data/icl.db"
# Server Configuration
PORT = 3000
NODE_ENV = development
Critical Security Settings: The default session secret in src/lib/session.ts is: password : "icl-secret-key-at-least-32-characters-long!!"
This must be changed in production via the SESSION_SECRET environment variable. Generate a secure key: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Environment Variable Reference: Variable Description Default Required SESSION_SECRETIron-session encryption key Hardcoded Yes (production) SESSION_SECUREEnable HTTPS-only cookies falseYes (production) DATABASE_PATHSQLite database file location ./data/icl.dbNo PORTHTTP server port 3000No NODE_ENVEnvironment (development/production) developmentNo
Initialize Database
Create the database directory and initialize schema: # Create data directory
mkdir -p data
# Generate Drizzle schema
npm run db:generate
# Run migrations (creates tables)
npm run db:migrate
# Seed with sample data (optional for development)
npm run db:seed
Database Schema: The schema is defined in src/db/schema.ts: src/db/schema.ts - Users
src/db/schema.ts - Clients
src/db/schema.ts - Quotations
export const users = sqliteTable ( "users" , {
id: integer ( "id" ). primaryKey ({ autoIncrement: true }),
full_name: text ( "full_name" ). notNull (),
email: text ( "email" ). notNull (). unique (),
password: text ( "password" ). notNull (),
role: text ( "role" , {
enum: [ "DIRECTOR" , "GERENTE" , "COMERCIAL" ,
"CSV" , "OPERACIONES" , "ADMINISTRACION" ]
}). notNull (),
is_active: integer ( "is_active" , { mode: "boolean" })
. notNull (). default ( true ),
dni: text ( "dni" ),
created_at: text ( "created_at" ). notNull ()
. default ( "(datetime('now'))" ),
updated_at: text ( "updated_at" ). notNull ()
. default ( "(datetime('now'))" ),
});
SQLite Configuration: The database connection in src/db/index.ts uses: import Database from "better-sqlite3" ;
const dbPath = path . join ( process . cwd (), "data" , "icl.db" );
const sqlite = new Database ( dbPath );
// Enable WAL mode for concurrent reads
sqlite . pragma ( "journal_mode = WAL" );
// Enable foreign key constraints
sqlite . pragma ( "foreign_keys = ON" );
WAL (Write-Ahead Logging) mode allows multiple concurrent readers while a write is in progress, improving performance.
Create Initial Admin User
If you didn’t run db:seed, create an admin user manually: // Create a script: scripts/create-admin.ts
import { db } from "@/db" ;
import { users } from "@/db/schema" ;
import { hashSync } from "bcryptjs" ;
const hashedPassword = hashSync ( "your-secure-password" , 10 );
db . insert ( users ). values ({
full_name: "System Administrator" ,
email: "[email protected] " ,
password: hashedPassword ,
role: "DIRECTOR" ,
is_active: true ,
});
console . log ( "Admin user created" );
Run it: tsx scripts/create-admin.ts
Store admin credentials securely. Never commit passwords to version control.
Development Server
Start the development server: The application runs on http://localhost:3000 with:
Hot module replacement
Turbopack for fast builds
Detailed error overlay
Available Scripts: {
"scripts" : {
"dev" : "next dev --turbopack" ,
"build" : "next build" ,
"start" : "next start" ,
"db:generate" : "drizzle-kit generate" ,
"db:migrate" : "drizzle-kit migrate" ,
"db:seed" : "tsx src/db/seed.ts"
}
}
Production Deployment
Build for Production
Build Application
Create an optimized production build: This generates:
Optimized JavaScript bundles
Server-side rendering configuration
Static assets
Production API routes
Output: Route (app) Size First Load JS
┌ ○ / 142 B 87.9 kB
├ ○ /login 142 B 87.9 kB
â”” Æ’ /cotizaciones 5.2 kB 93.1 kB
Start Production Server
Run the production server: Or with custom port:
Process Management
Using PM2 (Recommended)
# Install PM2 globally
npm install -g pm2
# Start application
pm2 start npm --name "iclapp" -- start
# Enable startup on boot
pm2 startup
pm2 save
# Monitor
pm2 logs iclapp
pm2 monit
PM2 Ecosystem File:
module . exports = {
apps: [{
name: 'iclapp' ,
script: 'npm' ,
args: 'start' ,
env: {
NODE_ENV: 'production' ,
PORT: 3000 ,
SESSION_SECRET: process . env . SESSION_SECRET ,
SESSION_SECURE: 'true' ,
},
instances: 1 ,
exec_mode: 'fork' ,
max_memory_restart: '1G' ,
error_file: './logs/err.log' ,
out_file: './logs/out.log' ,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z' ,
}]
};
Start with ecosystem file:
pm2 start ecosystem.config.js
Using systemd
/etc/systemd/system/iclapp.service
[Unit]
Description =ICL Cotizaciones
After =network.target
[Service]
Type =simple
User =iclapp
WorkingDirectory =/opt/iclapp
Environment = "NODE_ENV=production"
Environment = "PORT=3000"
Environment = "SESSION_SECRET=your-secret-key"
Environment = "SESSION_SECURE=true"
ExecStart =/usr/bin/npm start
Restart =on-failure
[Install]
WantedBy =multi-user.target
Enable and start:
sudo systemctl enable iclapp
sudo systemctl start iclapp
sudo systemctl status iclapp
Reverse Proxy Configuration
/etc/nginx/sites-available/iclapp
server {
listen 80 ;
listen [::]:80;
server_name cotizaciones.yourcompany.com;
# Redirect to HTTPS
return 301 https://$ server_name $ request_uri ;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name cotizaciones.yourcompany.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourcompany.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Proxy to Next.js
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1 ;
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection 'upgrade' ;
proxy_set_header Host $ host ;
proxy_set_header X-Real-IP $ remote_addr ;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $ scheme ;
proxy_cache_bypass $ http_upgrade ;
}
# Client body size limit (for file uploads)
client_max_body_size 10M ;
}
Enable site:
sudo ln -s /etc/nginx/sites-available/iclapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
cotizaciones.yourcompany.com {
reverse_proxy localhost:3000
encode gzip
# Automatic HTTPS with Let's Encrypt
tls [email protected]
}
Start Caddy:
caddy run --config Caddyfile
Database Backup Strategy
SQLite databases can become corrupted if not backed up properly. Always use WAL mode and proper backup procedures.
Automated Backup Script:
#!/bin/bash
set -e
# Configuration
DB_PATH = "./data/icl.db"
BACKUP_DIR = "./backups"
TIMESTAMP = $( date +"%Y%m%d_%H%M%S" )
BACKUP_FILE = "${ BACKUP_DIR }/icl_${ TIMESTAMP }.db"
# Create backup directory
mkdir -p "${ BACKUP_DIR }"
# SQLite online backup (safe while app is running)
sqlite3 "${ DB_PATH }" ".backup '${ BACKUP_FILE }'"
# Compress
gzip "${ BACKUP_FILE }"
# Delete backups older than 30 days
find "${ BACKUP_DIR }" -name "icl_*.db.gz" -mtime +30 -delete
echo "Backup completed: ${ BACKUP_FILE }.gz"
Make executable and schedule:
chmod +x scripts/backup-db.sh
# Add to crontab (daily at 2 AM)
crontab -e
0 2 * * * /opt/iclapp/scripts/backup-db.sh >> /var/log/iclapp-backup.log 2>&1
Monitoring & Logging
Application Logs
Next.js logs to stdout/stderr by default. Capture with PM2 or systemd.
Database Monitoring
# Check database size
du -h data/icl.db
# Check WAL size (should be small, auto-checkpointed)
du -h data/icl.db-wal
# Vacuum database (optimize, run during low traffic)
sqlite3 data/icl.db "VACUUM;"
Health Check Endpoint
Create src/app/api/health/route.ts:
import { NextResponse } from "next/server" ;
import { db } from "@/db" ;
import { sql } from "drizzle-orm" ;
export async function GET () {
try {
// Test database connection
const result = db . select ( sql `1 as ok` ). get ();
return NextResponse . json ({
status: "healthy" ,
timestamp: new Date (). toISOString (),
database: result ? "connected" : "disconnected" ,
});
} catch ( error ) {
return NextResponse . json (
{
status: "unhealthy" ,
error: String ( error )
},
{ status: 503 }
);
}
}
Monitor with:
curl http://localhost:3000/api/health
Security Hardening
Session Security
Change SESSION_SECRET to 32+ character random string
Enable SESSION_SECURE=true with HTTPS
Set httpOnly: true for cookies (default)
Consider shorter session TTL for sensitive data
Database Security
Restrict file permissions: chmod 600 data/icl.db
Run application as non-root user
Enable foreign key constraints (default)
Regular backups with encryption
Network Security
Always use HTTPS in production
Configure firewall (only 80/443 exposed)
Use reverse proxy for rate limiting
Enable CORS if needed (API only)
Application Security
Keep dependencies updated: npm audit
Use environment variables for secrets
Implement CSRF protection (Next.js default)
Regular security audits
Troubleshooting
SQLite in WAL mode should prevent most locks. If issues persist: # Check for stale locks
lsof data/icl.db
# Kill stale processes
# Stop app, remove WAL files, restart
rm data/icl.db-wal data/icl.db-shm
Monitor with PM2: pm2 monit
# Restart if memory exceeds threshold
pm2 restart iclapp
Check for:
Unclosed database connections
Memory-intensive queries
Large result sets not paginated
Verify:
Cookie is HTTP-only and secure matches protocol
Domain matches your deployment URL
Session secret is set correctly
Browser accepts cookies
Debug: // Check session in API route
const session = await getSession ();
console . log ( 'Session:' , session );
Build fails with native module error
better-sqlite3 requires build tools:Ubuntu/Debian: sudo apt-get install build-essential python3
npm rebuild better-sqlite3
CentOS/RHEL: sudo yum groupinstall "Development Tools"
npm rebuild better-sqlite3
Alpine Linux (Docker): RUN apk add --no-cache python3 make g++
Database Indexes
Add indexes for common queries:
-- Index quotation queries by date and status
CREATE INDEX idx_quotations_date ON quotations( date );
CREATE INDEX idx_quotations_status ON quotations( status );
CREATE INDEX idx_quotations_user_id ON quotations(user_id);
-- Index client lookups
CREATE INDEX idx_clients_user_id ON clients(user_id);
CREATE INDEX idx_clients_legal_name ON clients(legal_name);
-- Composite index for filtered queries
CREATE INDEX idx_quotations_user_status_date
ON quotations(user_id, status , date );
Caching Strategy
Implement Next.js caching for static data:
// Cache location data (rarely changes)
export const revalidate = 3600 ; // 1 hour
export async function GET () {
const locations = await db . select (). from ( locations );
return NextResponse . json ( locations );
}
Connection Pooling
For high-traffic deployments, consider connection pooling:
// src/db/index.ts
import Database from "better-sqlite3" ;
const pool = [
new Database ( dbPath , { readonly: true }),
new Database ( dbPath , { readonly: true }),
new Database ( dbPath , { readonly: true }),
];
let currentIndex = 0 ;
export function getReadConnection () {
currentIndex = ( currentIndex + 1 ) % pool . length ;
return pool [ currentIndex ];
}
Next Steps
Quickstart Create your first quotation
User Management Set up user roles and permissions
API Reference Explore the REST API
Backup & Recovery Database backup best practices