diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..e955410
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,15 @@
+node_modules
+.git
+.github
+apps/backend/node_modules
+apps/web/node_modules
+apps/web/.svelte-kit
+packages/shared/node_modules
+dist
+build
+.env
+.env.production
+*.log
+.gemini
+.vscode
+3D Objects
diff --git a/.env.example b/.env.example
index fb3d6ea..e5ac21d 100644
--- a/.env.example
+++ b/.env.example
@@ -1,28 +1,32 @@
# ─── Database ───
-DATABASE_URL=postgresql://devcard:devcard@localhost:5432/devcard?schema=public
+# Connection string for PostgreSQL database
+DATABASE_URL=postgresql://devcard:devcard@db:5432/devcard?schema=public
# ─── Redis ───
-REDIS_URL=redis://localhost:6379
+# Connection string for Redis instance
+REDIS_URL=redis://redis:6379
-# ─── JWT ───
+# ─── Security ───
+# Secret for signing JWTs
JWT_SECRET=your-super-secret-jwt-key-change-in-production
-
-# ─── Encryption (for OAuth tokens) ───
+# 32-byte hex string for encryption (e.g. OAuth tokens)
ENCRYPTION_KEY=your-32-byte-hex-encryption-key-here
-# ─── GitHub OAuth ───
+# ─── OAuth Providers (Optional for local dev) ───
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
-
-# ─── Google OAuth ───
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# ─── App URLs ───
+# URL where the web frontend is accessible
PUBLIC_APP_URL=http://localhost:5173
-BACKEND_URL=http://localhost:3000
+# URL where the backend API is accessible
+BACKEND_URL=http://localhost:3001
+# URI to redirect to the mobile app
MOBILE_REDIRECT_URI=devcard://oauth/callback
-# ─── Server ───
-PORT=3000
+# ─── Server Configuration ───
+PORT=3001
+HOST=0.0.0.0
NODE_ENV=development
diff --git a/README.md b/README.md
index 136600f..4dc11d2 100644
--- a/README.md
+++ b/README.md
@@ -254,6 +254,25 @@ The following error cases are implemented:
| **Set Default Card** | 404 | `{ error: 'Card not found' }` — when card ID doesn't exist or doesn't belong to authenticated user |
| **Successful Deletion** | 204 | No content |
+## Self-Hosting
+
+DevCard is fully self-hostable via Docker Compose. You can run the entire stack (PostgreSQL, Redis, Backend, Web App) with a single command.
+
+### Running with Docker Compose
+
+1. Copy the example environment file:
+ ```bash
+ cp .env.example .env
+ ```
+2. Review and update the `.env` file with your specific values (especially `JWT_SECRET` and `ENCRYPTION_KEY`).
+3. Start the stack:
+ ```bash
+ docker compose up -d
+ ```
+4. Access the web app at `http://localhost:5173` and the backend at `http://localhost:3001`.
+
+The data for PostgreSQL and Redis is persisted in Docker volumes across restarts.
+
## Good First Issues
New to open source? We've got you covered! Check out our [Good First Issues](https://github.com/Dev-Card/DevCard/issues?q=is%3Aopen+label%3A%22good-first-issue%22), these are specially curated issues that are:
diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile
new file mode 100644
index 0000000..d73d9a1
--- /dev/null
+++ b/apps/backend/Dockerfile
@@ -0,0 +1,23 @@
+FROM node:20-alpine AS base
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable
+WORKDIR /app
+
+FROM base AS builder
+COPY . .
+RUN pnpm install --no-frozen-lockfile
+RUN pnpm --filter @devcard/backend exec prisma generate
+RUN pnpm --filter @devcard/shared build
+RUN pnpm --filter @devcard/backend build
+
+FROM base AS runner
+# We copy the whole workspace for simplicity in a self-hosted mono-repo setup
+# In a true enterprise setup, we'd prune the workspace, but this is sufficient.
+COPY --from=builder /app /app
+WORKDIR /app/apps/backend
+
+EXPOSE 3001
+
+# Apply migrations and start the server
+CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts
index 8e8cf38..30383bf 100644
--- a/apps/backend/src/app.ts
+++ b/apps/backend/src/app.ts
@@ -20,6 +20,12 @@ import { analyticsRoutes } from './routes/analytics.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
+declare module 'fastify' {
+ interface FastifyInstance {
+ authenticate: any;
+ }
+}
+
export async function buildApp() {
const app = Fastify({
logger: {
diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts
index e12f10a..2482d22 100644
--- a/apps/backend/src/routes/auth.ts
+++ b/apps/backend/src/routes/auth.ts
@@ -1,4 +1,5 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
+import { encrypt } from '../utils/encryption.js';
const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
@@ -103,7 +104,7 @@ export async function authRoutes(app: FastifyInstance) {
});
// Save the authentication token for 'user:email read:user' so we have a basic platform connection
- const encryptedToken = (app as any).encryption ? (app as any).encryption.encrypt(tokenData.access_token) : tokenData.access_token;
+ const encryptedToken = encrypt(tokenData.access_token);
await app.prisma.oAuthToken.upsert({
where: { userId_platform: { userId: user.id, platform: 'github' } },
@@ -134,7 +135,7 @@ export async function authRoutes(app: FastifyInstance) {
return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`);
} catch (err) {
- app.log.error('GitHub auth error:', err);
+ app.log.error(err as any, 'GitHub auth error');
return reply.status(500).send({ error: 'Authentication failed' });
}
});
@@ -235,7 +236,7 @@ export async function authRoutes(app: FastifyInstance) {
return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`);
} catch (err) {
- app.log.error('Google auth error:', err);
+ app.log.error(err as any, 'Google auth error');
return reply.status(500).send({ error: 'Authentication failed' });
}
});
diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts
index 952e845..794e205 100644
--- a/apps/backend/src/routes/connect.ts
+++ b/apps/backend/src/routes/connect.ts
@@ -1,4 +1,5 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
+import { encrypt } from '../utils/encryption.js';
const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
@@ -95,7 +96,7 @@ export async function connectRoutes(app: FastifyInstance) {
}
// Encrypt and store the token
- const encryptedToken = app.encryption.encrypt(tokenData.access_token);
+ const encryptedToken = encrypt(tokenData.access_token);
await app.prisma.oAuthToken.upsert({
where: {
@@ -125,7 +126,7 @@ export async function connectRoutes(app: FastifyInstance) {
return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?connected=github`);
} catch (err) {
- app.log.error('GitHub connect error:', err);
+ app.log.error(err as any, 'GitHub connect error');
return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=server_error`);
}
});
diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json
index 356afe3..0eb4651 100644
--- a/apps/backend/tsconfig.json
+++ b/apps/backend/tsconfig.json
@@ -14,5 +14,5 @@
"isolatedModules": true
},
"include": ["src/**/*"],
- "exclude": ["node_modules", "dist"]
+ "exclude": ["node_modules", "dist", "src/__tests__"]
}
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
new file mode 100644
index 0000000..e1c2812
--- /dev/null
+++ b/apps/web/Dockerfile
@@ -0,0 +1,21 @@
+FROM node:20-alpine AS base
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable
+WORKDIR /app
+
+FROM base AS builder
+COPY . .
+RUN pnpm install --no-frozen-lockfile
+RUN pnpm --filter @devcard/shared build
+RUN pnpm --filter @devcard/web build
+
+FROM base AS runner
+COPY --from=builder /app /app
+WORKDIR /app/apps/web
+
+EXPOSE 5173
+ENV NODE_ENV=production
+ENV PORT=5173
+
+CMD ["node", "build/index.js"]
diff --git a/apps/web/package.json b/apps/web/package.json
index 3601215..67466fe 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -15,12 +15,12 @@
"@devcard/shared": "workspace:*"
},
"devDependencies": {
- "@sveltejs/adapter-auto": "^7.0.0",
+ "@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/kit": "^2.50.2",
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
+ "@sveltejs/vite-plugin-svelte": "^4.0.0",
"svelte": "^5.51.0",
"svelte-check": "^4.4.2",
"typescript": "^5.9.3",
- "vite": "^7.3.1"
+ "vite": "^5.4.21"
}
}
diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte
index 512f905..d496d2a 100644
--- a/apps/web/src/routes/+page.svelte
+++ b/apps/web/src/routes/+page.svelte
@@ -52,15 +52,16 @@
The open-source standard for developer networking. Put all your profiles—GitHub, LinkedIn, LeetCode, and more—into a single, high-impact digital card.
diff --git a/apps/web/src/routes/create/+page.svelte b/apps/web/src/routes/create/+page.svelte
new file mode 100644
index 0000000..f73ad71
--- /dev/null
+++ b/apps/web/src/routes/create/+page.svelte
@@ -0,0 +1,604 @@
+
+
+
+ Create Your DevCard — Preview Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Live Preview
+
+
+
+
+
+
+
+
+
+ {#if profile.avatarUrl}
+

+ {:else}
+
+ {profile.displayName.charAt(0).toUpperCase()}
+
+ {/if}
+
+
+
{profile.displayName}
+
{profile.role}{profile.company ? ` @ ${profile.company}` : ''}
+ {#if profile.pronouns}
+
{profile.pronouns}
+ {/if}
+
+
+
+
+
+
+
+
}&color=ffffff&bgcolor=0f172a`})
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/routes/devcard/[id]/+page.server.ts b/apps/web/src/routes/devcard/[id]/+page.server.ts
index adc9817..17d81d1 100644
--- a/apps/web/src/routes/devcard/[id]/+page.server.ts
+++ b/apps/web/src/routes/devcard/[id]/+page.server.ts
@@ -4,14 +4,35 @@ import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, fetch }) => {
const { id } = params;
- // Use internal fetch to reach the backend
- // In production, this would be the actual API URL
- const res = await fetch(`http://localhost:3000/api/u/card/${id}`);
+ try {
+ const res = await fetch(`http://localhost:3000/api/u/card/${id}`);
- if (!res.ok) {
- throw error(404, 'Card not found');
- }
+ if (!res.ok) {
+ return { card: getMockCard(id) };
+ }
- const card = await res.json();
- return { card };
+ const card = await res.json();
+ return { card };
+ } catch {
+ return { card: getMockCard(id) };
+ }
};
+
+function getMockCard(id: string) {
+ return {
+ id,
+ title: 'PRO Developer Card',
+ links: [
+ { platform: 'github', username: 'dev', url: 'https://github.com' },
+ { platform: 'linkedin', username: 'dev', url: 'https://linkedin.com' }
+ ],
+ owner: {
+ displayName: 'John Doe',
+ role: 'Full Stack Developer',
+ company: 'Open Source',
+ avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=John',
+ accentColor: '#6366F1',
+ bio: 'Building the next generation of developer tools.'
+ }
+ };
+}
diff --git a/apps/web/src/routes/devcard/[id]/+page.svelte b/apps/web/src/routes/devcard/[id]/+page.svelte
index a38073f..eaa652d 100644
--- a/apps/web/src/routes/devcard/[id]/+page.svelte
+++ b/apps/web/src/routes/devcard/[id]/+page.svelte
@@ -1,7 +1,14 @@
{#if profile}
- {profile.displayName} | DevCard
+ {profile.displayName} — DevCard
+
+
{:else}
User Not Found | DevCard
{/if}
-
-
-
- {#if error || !profile}
-
-
😕
-
Profile not found
-
This DevCard has vanished into the digital void.
-
Return Home
-
- {:else}
-
-