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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
.github
.next
.vercel
node_modules
npm-debug.log*
temp-query.js
.env*
!.env.example
docs
docs-site
tests
78 changes: 33 additions & 45 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,64 +1,52 @@
name: Deploy to Vercel
name: Deploy web app to Azure

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
AZURE_RESOURCE_GROUP: devglobe-rg
AZURE_CONTAINER_REGISTRY: devglobewebacr
AZURE_CONTAINER_APP: devglobe-web
IMAGE_NAME: devglobe-web
NEXT_PUBLIC_API_URL: https://devglobe-public-api.azurewebsites.net
NEXT_PUBLIC_DEVELOPER_SNAPSHOT_URL: https://devglobeactivityfn.z13.web.core.windows.net/developers.json
NEXT_PUBLIC_SITE_URL: https://www.devglobe.dev

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
- name: Log in to Azure
uses: azure/login@v2
with:
node-version: 22
cache: npm

- name: Use repository npm version
run: npm install --global npm@11.14.1

- name: Install dependencies
run: npm ci

- name: Install Vercel CLI
run: npm install -g vercel

- name: Pull Vercel Environment
if: env.VERCEL_TOKEN != '' && env.VERCEL_ORG_ID != '' && env.VERCEL_PROJECT_ID != ''
run: vercel pull --yes --environment=${{ github.event_name == 'push' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

- name: Build
if: env.VERCEL_TOKEN != '' && env.VERCEL_ORG_ID != '' && env.VERCEL_PROJECT_ID != ''
run: vercel build ${{ github.event_name == 'push' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}

- name: Deploy
id: deploy
if: env.VERCEL_TOKEN != '' && env.VERCEL_ORG_ID != '' && env.VERCEL_PROJECT_ID != ''
- name: Build and push image
run: |
URL=$(vercel deploy --prebuilt ${{ github.event_name == 'push' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }})
echo "url=$URL" >> $GITHUB_OUTPUT

- name: Comment PR with Preview URL
if: github.event_name == 'pull_request' && env.VERCEL_TOKEN != '' && env.VERCEL_ORG_ID != '' && env.VERCEL_PROJECT_ID != ''
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 Preview deployed: ${{ steps.deploy.outputs.url }}`
})
az acr build \
--registry "$AZURE_CONTAINER_REGISTRY" \
--image "$IMAGE_NAME:${GITHUB_SHA}" \
--build-arg NEXT_PUBLIC_API_URL="$NEXT_PUBLIC_API_URL" \
--build-arg NEXT_PUBLIC_DEVELOPER_SNAPSHOT_URL="$NEXT_PUBLIC_DEVELOPER_SNAPSHOT_URL" \
--build-arg NEXT_PUBLIC_SITE_URL="$NEXT_PUBLIC_SITE_URL" \
.

- name: Deploy image
run: |
az containerapp update \
--resource-group "$AZURE_RESOURCE_GROUP" \
--name "$AZURE_CONTAINER_APP" \
--image "$AZURE_CONTAINER_REGISTRY.azurecr.io/$IMAGE_NAME:${GITHUB_SHA}"

- name: Log out of Azure
if: always()
run: az logout
2 changes: 2 additions & 0 deletions .github/workflows/docs-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ jobs:

- name: Verify hosted agent resources
run: npm run docs:check-live
env:
LIVE_SITE_URL: https://devglobe-web.victoriousground-cbcea081.eastus.azurecontainerapps.io

- name: Run application tests
run: npm test
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ dist/
dashboards/
docs/adx-growth-dashboard.md
scripts/build-devglobe-growth-dashboard.cjs
.env*.local
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
FROM node:22-slim AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --global npm@11.14.1 \
&& npm ci

FROM node:22-slim AS builder
WORKDIR /app
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_DEVELOPER_SNAPSHOT_URL
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_DEVELOPER_SNAPSHOT_URL=$NEXT_PUBLIC_DEVELOPER_SNAPSHOT_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-slim AS runner
WORKDIR /app
ENV HOSTNAME=0.0.0.0
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**The global open-source developer discovery platform — search, compare, and connect with the people behind the code**

[![Live Demo](https://img.shields.io/badge/Live-Demo-blue?style=for-the-badge&logo=vercel)](https://devglobe.dev)
[![Live Demo](https://img.shields.io/badge/Live-Demo-blue?style=for-the-badge&logo=microsoftazure)](https://devglobe.dev)
[![Documentation](https://img.shields.io/badge/Documentation-GitHub%20Pages-2ea44f?style=for-the-badge&logo=github)](https://sajeetharan.github.io/devglobe/)
[![VS Code](https://img.shields.io/badge/VS%20Code-Install-007ACC?style=for-the-badge&logo=visualstudiocode)](https://marketplace.visualstudio.com/items?itemName=devglobedev.devglobe-developer-discovery)
[![GitHub Stars](https://img.shields.io/github/stars/sajeetharan/devglobe?style=for-the-badge&logo=github)](https://github.com/sajeetharan/devglobe/stargazers)
Expand All @@ -19,7 +19,7 @@

DevGlobe is an interactive global developer network built for engineering teams, open-source communities, and the emerging ecosystem of AI agents. It combines a 3D developer map with Azure Cosmos DB vector and hybrid search to surface relevant expertise from real contribution signals rather than popularity alone. The long-term vision is a consent-aware discovery layer where AI agents can find the right human collaborators.

The dynamic application is hosted on [Vercel](https://www.devglobe.dev). Product, API, MCP, Agent Skill, and agent-readiness documentation is published separately on [GitHub Pages](https://sajeetharan.github.io/devglobe/).
The dynamic application is hosted on [Azure Container Apps](https://www.devglobe.dev). Product, API, MCP, Agent Skill, and agent-readiness documentation is published separately on [GitHub Pages](https://sajeetharan.github.io/devglobe/).

> [!IMPORTANT]
> **Connect an AI agent to DevGlobe:** MCP-compatible agents can use the hosted endpoint at `https://www.devglobe.dev/mcp` to search public developer profiles without credentials. Verified agents can also request developer-approved introductions. See the [MCP setup guide](docs/mcp-server.md).
Expand Down Expand Up @@ -111,7 +111,7 @@ AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o-mini

`AZURE_OPENAI_CHAT_DEPLOYMENT` enables generated fun facts on identity cards. Cards use factual profile copy when the chat deployment is unavailable.

Production can serve high-volume public API reads and the developer snapshot directly from Azure while Vercel hosts the frontend. See [docs/azure-backend.md](docs/azure-backend.md) for the resource layout, environment switches, and deployment checks.
Production serves the Next.js application from Azure Container Apps, high-volume public API reads from Azure Functions, and the developer snapshot from Azure Blob Storage. See [docs/azure-backend.md](docs/azure-backend.md) for the resource layout, environment switches, and deployment checks.

```bash
npm run dev
Expand All @@ -124,7 +124,7 @@ npm run dev
| Frontend | React 19, Three.js (react-globe.gl), Next.js 15 |
| Search | Azure Cosmos DB (vector + hybrid search) |
| API | Next.js API Routes |
| Hosting | Vercel |
| Hosting | Azure Container Apps and Azure Functions |
| Data Pipeline | Node.js scripts (GitHub GraphQL, StackOverflow API, geocoding) |

## 📊 Scoring Formula (0–100)
Expand Down Expand Up @@ -201,11 +201,9 @@ The command preserves existing tags and is idempotent. Other credentials require
└── developers-sample.json # Sample data for local dev
```

## 🌍 Deploy to Vercel
## 🌍 Deploy to Azure Container Apps

```bash
npx vercel
```
Pushes to `main` build the standalone Next.js image in Azure Container Registry and deploy it to Azure Container Apps through GitHub Actions OIDC. The deployment identity requires Contributor access scoped to the application resource group and these repository variables: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID`.

Required environment variables:

Expand Down Expand Up @@ -233,7 +231,7 @@ npm run setup-contacts-container

See the [lifecycle email PRD](docs/prd/lifecycle-email-notifications.md).

Verified users can explicitly opt in to a Monday weekly digest from the user menu. The digest includes current global and country rankings, rank movement since the previous digest, current DevGlobe features, and an Explore DevGlobe link. Vercel invokes `/api/cron/weekly-digest` at 13:00 UTC each Monday; only verified contacts with `productUpdatesEnabled: true` are eligible. Each message uses a per-user, per-week idempotency key and includes one-click unsubscribe headers and a signed unsubscribe link.
Verified users can explicitly opt in to a Monday weekly digest from the user menu. The digest includes current global and country rankings, rank movement since the previous digest, current DevGlobe features, and an Explore DevGlobe link. The Azure Functions app invokes `/api/cron/weekly-digest` at 13:00 UTC each Monday; only verified contacts with `productUpdatesEnabled: true` are eligible. Each message uses a per-user, per-week idempotency key and includes one-click unsubscribe headers and a signed unsubscribe link.

Generate a manual-review activation queue and weekly social spotlight from public, unclaimed profiles:

Expand Down Expand Up @@ -266,10 +264,10 @@ Deploy the `functions` directory to an Azure Function App and configure these ap

```env
IMPACT_HISTORY_URL=https://www.devglobe.dev/api/cron/impact-history
CRON_SECRET=the-same-secret-configured-in-vercel
CRON_SECRET=the-same-secret-configured-on-the-container-app
```

The timer resumes the current UTC day's capture in RU-bounded batches. Keep `IMPACT_HISTORY_CONCURRENCY` and `IMPACT_HISTORY_BATCH_SIZE` on the Vercel application because the Next.js endpoint performs the Cosmos work.
The timer resumes the current UTC day's capture in RU-bounded batches. Keep `IMPACT_HISTORY_CONCURRENCY` and `IMPACT_HISTORY_BATCH_SIZE` on the Container App because the Next.js endpoint performs the Cosmos work.

### Email verification reminders

Expand All @@ -279,10 +277,11 @@ Configure these application settings on the Azure Function App:

```env
EMAIL_VERIFICATION_REMINDERS_URL=https://www.devglobe.dev/api/cron/email-verification-reminders
CRON_SECRET=the-same-secret-configured-in-vercel
WEEKLY_DIGEST_URL=https://www.devglobe.dev/api/cron/weekly-digest
CRON_SECRET=the-same-secret-configured-on-the-container-app
```

Deploy the complete `functions` directory so the timer and its `function.json` are included. Do not configure the same reminder schedule in Vercel, or each due batch may be invoked twice.
Deploy the complete `functions` directory so each timer and its `function.json` are included.

## 🤝 Contributing

Expand Down
2 changes: 0 additions & 2 deletions app/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Analytics } from '@vercel/analytics/react';
import '../styles/main.css';
import { getSiteUrl } from '../lib/site.js';
import WebMcpProvider from '../components/WebMcpProvider.jsx';
Expand Down Expand Up @@ -160,7 +159,6 @@ export default function RootLayout({ children }) {
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData).replace(/</g, '\\u003c') }}
/>
{children}
<Analytics />
</body>
</html>
);
Expand Down
2 changes: 1 addition & 1 deletion app/page.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import React, { useState, useEffect, useCallback, useRef } from 'react';
import { track } from '@vercel/analytics';
import { track } from '../lib/analytics.js';
import Header from '../components/Header.jsx';
import SearchBar from '../components/SearchBar.jsx';
import Leaderboard from '../components/Leaderboard.jsx';
Expand Down
2 changes: 1 addition & 1 deletion components/AddMeModal.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { track } from '@vercel/analytics';
import { track } from '../lib/analytics.js';
import styles from './AddMeModal.module.css';

const PENDING_CLAIM_KEY = 'devglobe-pending-claim';
Expand Down
2 changes: 1 addition & 1 deletion components/AgentSetupPage.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import Link from 'next/link';
import { track } from '@vercel/analytics';
import { track } from '../lib/analytics.js';
import { useEffect, useState } from 'react';
import styles from './AgentSetupPage.module.css';

Expand Down
2 changes: 1 addition & 1 deletion components/DetailPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React, { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { track } from '@vercel/analytics';
import { track } from '../lib/analytics.js';
import * as d3 from 'd3';
import { formatNum, formatRelativeTime, formatUsd, isStaleData } from '../lib/format.js';
import { DIMENSIONS, SCORE_METHODOLOGY } from '../lib/scoring.js';
Expand Down
2 changes: 1 addition & 1 deletion components/DeveloperActivityPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Link from 'next/link';
import { useEffect, useState } from 'react';
import { track } from '@vercel/analytics';
import { track } from '../lib/analytics.js';
import { formatNum, formatRelativeTime } from '../lib/format.js';
import { publicApiUrl } from '../lib/public-api.js';
import { useActivityFeed } from './useActivityFeed.js';
Expand Down
2 changes: 1 addition & 1 deletion components/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import React from 'react';
import { track } from '@vercel/analytics';
import { track } from '../lib/analytics.js';
import UserMenu from './UserMenu.jsx';

const marketplaceUrl = 'https://marketplace.visualstudio.com/items?itemName=devglobedev.devglobe-developer-discovery';
Expand Down
2 changes: 1 addition & 1 deletion components/UserMenu.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import React, { useState, useRef, useEffect } from 'react';
import { track } from '@vercel/analytics';
import { track } from '../lib/analytics.js';

export default function UserMenu({ user, onLogout, onClaim, onEditAiProfile, onOpenIntroductions, claimStatus }) {
const [open, setOpen] = useState(false);
Expand Down
13 changes: 13 additions & 0 deletions functions/weekly-digest/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"bindings": [
{
"name": "timer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 0 13 * * 1",
"runOnStartup": false,
"useMonitor": true
}
],
"scriptFile": "index.js"
}
20 changes: 20 additions & 0 deletions functions/weekly-digest/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = async function weeklyDigest(context) {
const endpoint = process.env.WEEKLY_DIGEST_URL;
const secret = process.env.CRON_SECRET;
if (!endpoint || !secret) {
throw new Error('WEEKLY_DIGEST_URL and CRON_SECRET are required');
}

const response = await fetch(endpoint, {
headers: { Authorization: `Bearer ${secret}` },
});
const result = await response.json();
context.log('DevGlobe weekly digest', {
status: response.status,
eligible: result.eligible,
sent: result.sent,
skipped: result.skipped,
failed: result.failed,
});
if (!response.ok) throw new Error(`Weekly digest returned ${response.status}`);
};
1 change: 1 addition & 0 deletions lib/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function track() {}
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
outputFileTracingIncludes: {
'/api/card': ['./node_modules/@fontsource/manrope/files/manrope-latin-*-normal.woff'],
},
Expand Down
Loading