Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ FROM node:22-alpine AS build
WORKDIR /app

COPY package.json package-lock.json ./

COPY prisma ./prisma
RUN npm install -g npm@11
RUN npm ci

COPY . .

RUN npx prisma generate \
RUN DATABASE_URL="postgresql://build:build@localhost:5432/build" npx prisma generate \
&& npm run build \
&& npm prune --omit=dev

Expand All @@ -25,8 +26,9 @@ ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/prisma.config.ts ./
COPY --from=build /app/package.json ./
COPY --from=build /app/scripts ./scripts

EXPOSE 3000

CMD ["node", "dist/server.js"]
CMD ["sh", "scripts/start.sh"]
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
services:
app:
build: .
container_name: linkforge-app
restart: unless-stopped
ports:
- '3000:3000'
environment:
DATABASE_URL: postgresql://linkforge:linkforge@postgres:5432/linkforge
REDIS_URL: redis://redis:6379
JWT_SECRET: dev-only-change-me-to-a-32-char-minimum-secret
IP_HASH_SALT: dev-only-16-chars-min
BASE_URL: http://localhost:3000
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy

postgres:
image: postgres:16-alpine
container_name: linkforge-postgres
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "linkforge",
"version": "0.6.0",
"version": "0.6.3",
"description": "URL shortener API with authentication, API keys, async click tracking and analytics.",
"type": "module",
"license": "MIT",
Expand Down
3 changes: 3 additions & 0 deletions prisma.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { defineConfig, env } from 'prisma/config';
import 'dotenv/config';

export default defineConfig({
migrations: {
seed: 'tsx prisma/seed.ts',
},
datasource: {
url: env('DATABASE_URL'),
},
Expand Down
65 changes: 65 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import * as argon2 from 'argon2';
import 'dotenv/config';
import { createHash, randomBytes } from 'node:crypto';

const prisma = new PrismaClient({
adapter: new PrismaPg(process.env['DATABASE_URL']!),
});

const DEMO_EMAIL = 'demo@linkforge.dev';
const DEMO_PASSWORD = 'DemoUser2026!';
const COUNTRIES = ['FR', 'US', 'DE', 'GB', 'JP', 'BR'] as const;
const REFERRERS = ['google.com', 'twitter.com', 'news.ycombinator.com', null] as const;
const DEVICES = ['desktop', 'mobile', 'tablet'] as const;
const BROWSERS = ['Chrome', 'Safari', 'Firefox', 'Edge'] as const;

function pick<T>(arr: readonly T[]): T {
return arr[Math.floor(Math.random() * arr.length)] as T;
}

async function main(): Promise<void> {
console.log('Seeding demo data...');

// Idempotent: re-running the seed wipes the demo account and rebuilds it.
await prisma.user.deleteMany({ where: { email: DEMO_EMAIL } });

const user = await prisma.user.create({
data: { email: DEMO_EMAIL, password: await argon2.hash(DEMO_PASSWORD) },
});

const links = await Promise.all(
[
{ code: 'launch1', target: 'https://example.com/launch' },
{ code: 'docs01a', target: 'https://example.com/docs' },
{ code: 'blogpst', target: 'https://example.com/blog/announcement' },
].map((l) => prisma.link.create({ data: { ...l, userId: user.id } })),
);

// 90 days of clicks, weighted toward recent days.
const now = Date.now();
const clickData = Array.from({ length: 600 }, () => {
const daysAgo = Math.floor(Math.random() ** 1.6 * 90);
return {
linkId: pick(links).id,
country: pick(COUNTRIES),
deviceType: pick(DEVICES),
browser: pick(BROWSERS),
referrerHost: pick(REFERRERS),
ipHash: createHash('sha256').update(randomBytes(16)).digest('hex').slice(0, 16),
createdAt: new Date(now - daysAgo * 86_400_000 - Math.random() * 86_400_000),
};
});
await prisma.click.createMany({ data: clickData });

console.log(`Done. Demo user: ${DEMO_EMAIL} / ${DEMO_PASSWORD}`);
console.log(`Created ${links.length} links and ${clickData.length} clicks.`);
}

main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => void prisma.$disconnect());
30 changes: 30 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Blueprint for Render. Connect this repo in the Render dashboard and the
# web service is provisioned from this file. Postgres and Redis live on Neon
# and Upstash respectively; their connection strings are injected via env vars.
services:
- type: web
name: linkforge
runtime: docker
plan: free
region: frankfurt
healthCheckPath: /health
autoDeploy: true
envVars:
- key: NODE_ENV
value: production
- key: HOST
value: 0.0.0.0
- key: PORT
value: 3000
- key: LOG_LEVEL
value: info
- key: BASE_URL
sync: false # set in Render dashboard to https://<service>.onrender.com
- key: DATABASE_URL
sync: false # paste the Neon pooled connection string
- key: REDIS_URL
sync: false # paste the Upstash rediss:// URL
- key: JWT_SECRET
generateValue: true
- key: IP_HASH_SALT
generateValue: true
10 changes: 10 additions & 0 deletions scripts/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh
set -e

# Apply database migrations before booting the API.
# Safe to run on every cold start: prisma migrate deploy is idempotent.
echo "Applying database migrations..."
npx prisma migrate deploy

echo "Starting LinkForge..."
exec node dist/server.js
Loading