diff --git a/services/targo-hub/lib/signup.js b/services/targo-hub/lib/signup.js
new file mode 100644
index 0000000..e96a336
--- /dev/null
+++ b/services/targo-hub/lib/signup.js
@@ -0,0 +1,129 @@
+'use strict'
+/**
+ * signup.js — portail self-service d'abonnement (STAGING, servi par le hub).
+ * Parcours épuré type e-commerce : Forfait → Coordonnées → Récap → Confirmation.
+ * Réutilise /api/catalog (existant). PAS publié sur Lovable tant que non validé.
+ *
+ * Routes publiques :
+ * GET /signup → page (stepper)
+ * POST /signup/submit → crée un Lead ERPNext + retourne la confirmation
+ */
+const { json, parseBody, erpFetch } = require('./helpers')
+
+async function createLead (body) {
+ const name = [body.first_name, body.last_name].filter(Boolean).join(' ').trim() || 'Client web'
+ const payload = {
+ lead_name: name,
+ email_id: body.email || '',
+ mobile_no: body.phone || '',
+ status: 'Lead',
+ // résumé de la demande dans le titre de la requête pour le suivi staff
+ request_type: 'Product Enquiry',
+ }
+ try {
+ const r = await erpFetch('/api/resource/Lead', { method: 'POST', body: JSON.stringify(payload) })
+ if (r.status === 200 && r.data?.data) return r.data.data.name
+ } catch (e) { /* best-effort */ }
+ return null
+}
+
+const PAGE = `
Abonnement Gigafibre
+
+`
+
+async function handle (req, res, method, path) {
+ if (path === '/signup' && method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(PAGE) }
+ if (path === '/signup/submit' && method === 'POST') {
+ const b = await parseBody(req)
+ const lead = await createLead(b)
+ return json(res, 200, { ok: true, lead, plan: b.plan_name })
+ }
+ return json(res, 404, { error: 'not found' })
+}
+
+module.exports = { handle }
diff --git a/services/targo-hub/server.js b/services/targo-hub/server.js
index 29299a8..b406b78 100644
--- a/services/targo-hub/server.js
+++ b/services/targo-hub/server.js
@@ -130,6 +130,8 @@ const server = http.createServer(async (req, res) => {
if (path.startsWith('/roster')) return require('./lib/roster').handle(req, res, method, path, url)
// Portail public de prise de RDV (staging) — page + API client, PUBLIC (pas de SSO).
if (path === '/book' || path.startsWith('/book/')) return require('./lib/roster').handlePublicBooking(req, res, method, path, url)
+ // Portail self-service d'abonnement (staging) — page + submit, PUBLIC.
+ if (path === '/signup' || path.startsWith('/signup/')) return require('./lib/signup').handle(req, res, method, path)
if (path.startsWith('/campaigns')) return require('./lib/campaigns').handle(req, res, method, path)
// Gift redirect wrapper — short public URLs in campaign emails that
// 302 to the underlying Giftbit shortlink (subject to our expiry/revoke).