Replit Core Workflow A — Full-Stack App
Overview
Build a production-ready web app on Replit: Express or Flask server, PostgreSQL database, Replit Auth for user login, Object Storage for file uploads, and Autoscale deployment. This is the primary money-path workflow for shipping apps on Replit.
Prerequisites
- Replit account (Core plan or higher for deployments)
.replitandreplit.nixconfigured (seereplit-install-auth)- PostgreSQL provisioned in the Database pane
Instructions
Step 1: Project Structure
my-app/
├── .replit # Run + deployment config
├── replit.nix # System dependencies
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Express entry point
│ ├── routes/
│ │ ├── api.ts # API endpoints
│ │ ├── auth.ts # Auth routes
│ │ └── health.ts # Health check
│ ├── services/
│ │ ├── db.ts # PostgreSQL pool
│ │ └── storage.ts # Object Storage
│ └── middleware/
│ ├── auth.ts # Replit Auth middleware
│ └── errors.ts # Error handler
└── tests/
Step 2: Configuration Files
# .replit
entrypoint = "src/index.ts"
run = "npx tsx src/index.ts"
modules = ["nodejs-20:v8-20230920-bd784b9"]
[nix]
channel = "stable-24_05"
[env]
NODE_ENV = "development"
[deployment]
run = ["sh", "-c", "npx tsx src/index.ts"]
build = ["sh", "-c", "npm ci"]
deploymentTarget = "autoscale"
# replit.nix
{ pkgs }: {
deps = [
pkgs.nodejs-20_x
pkgs.nodePackages.typescript-language-server
pkgs.postgresql
];
}
Step 3: Database Layer
// src/services/db.ts
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
// Initialize schema
export async function initDB() {
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
profile_image TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
user_id TEXT REFERENCES users(id),
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
`);
}
export async function upsertUser(id: string, username: string, image: string) {
return pool.query(
`INSERT INTO users (id, username, profile_image)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE SET username = $2, profile_image = $3
RETURNING *`,
[id, username, image]
);
}
export async function createPost(userId: string, title: string, content: string) {
return pool.query(
'INSERT INTO posts (user_id, title, content) VALUES ($1, $2, $3) RETURNING *',
[userId, title, content]
);
}
export async function getPosts(limit = 20) {
return pool.query(
`SELECT p.*, u.username, u.profile_image
FROM posts p JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC LIMIT $1`,
[limit]
);
}
export { pool };
Step 4: Auth Middleware
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { upsertUser } from '../services/db';
export interface AuthedRequest extends Request {
user: { id: string; name: string; image: string };
}
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const userId = req.headers['x-replit-user-id'] as string;
if (!userId) return res.status(401).json({ error: 'Login required' });
const name = (req.headers['x-replit-user-name'] as string) || '';
const image = (req.headers['x-replit-user-profile-image'] as string) || '';
// Upsert user in database on every authenticated request
await upsertUser(userId, name, image);
(req as any).user = { id: userId, name, image };
next();
}
Step 5: API Routes
// src/routes/api.ts
import { Router } from 'express';
import { requireAuth, AuthedRequest } from '../middleware/auth';
import { createPost, getPosts } from '../services/db';
import { Client as StorageClient } from '@replit/object-storage';
const router = Router();
const storage = new StorageClient();
// Public: list posts
router.get('/posts', async (req, res) => {
const { rows } = await getPosts();
res.json(rows);
});
// Protected: create post
router.post('/posts', requireAuth, async (req, res) => {
const { title, content } = req.body;
const user = (req as AuthedRequest).user;
const { rows } = await createPost(user.id, title, content);
res.status(201).json(rows[0]);
});
// Protected: upload file to Object Storage
router.post('/upload', requireAuth, async (req, res) => {
const user = (req as AuthedRequest).user;
const filename = `uploads/${user.id}/${Date.now()}-${req.body.name}`;
await storage.uploadFromText(filename, req.body.content);
res.json({ path: filename });
});
export default router;
Step 6: Entry Point
// src/index.ts
import express from 'express';
import { initDB, pool } from './services/db';
import apiRoutes from './routes/api';
const app = express();
app.use(express.json());
// Health check (required for Replit deployments)
app.get('/health', async (req, res) => {
try {
await pool.query('SELECT 1');
res.json({ status: 'ok', uptime: process.uptime() });
} catch {
res.status(503).json({ status: 'degraded' });
}
});
// Auth info endpoint (client-side: GET /__replauthuser)
app.get('/api/me', (req, res) => {
const id = req.headers['x-replit-user-id'];
if (!id) return res.json({ loggedIn: false });
res.json({
loggedIn: true,
id,
name: req.headers['x-replit-user-name'],
image: req.headers['x-replit-user-profile-image'],
});
});
app.use('/api', apiRoutes);
const PORT = parseInt(process.env.PORT || '3000');
initDB().then(() => {
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
});
Error Handling
| Error | Cause | Solution |
|-------|-------|----------|
| DATABASE_URL undefined | PostgreSQL not provisioned | Create database in Database pane |
| Auth headers empty | Running in dev mode | Auth only works on deployed .replit.app |
| Object Storage 403 | No bucket created | Provision bucket in Object Storage pane |
| Port conflict | Multiple services on same port | Use different ports, set ignorePorts |
Resources
Next Steps
For collaboration and admin workflows, see replit-core-workflow-b.