Repository navigation
Organizations, passkeys and a redesigned Envoy - #4
Merged
Merged
Conversation
- Add shadcn/ui (Radix) primitives under components/ui, restyled for Envoy: filled inputs, no drop shadows, one 10px corner radius, green accent (#00916C). - Theme tokens in globals.css: neutral surfaces, pastel tones for roles and environments, light and dark themes via next-themes. - Geist Sans and Mono fonts, new logo mark, theme toggle. - Replace tailwind.config.js with Tailwind v4 CSS config. - Dependencies for the rest of the work: swr, zod, otpauth, qrcode, cmdk, input-otp, sonner, server-only, simple-icons, tsx.
- Encrypt values with AES-256-GCM (enc:v1: prefix) using a server-only ENCRYPTION_SECRET; legacy CryptoJS values still decrypt. - Rate limiting read the client IP from a plain headers object, so every request shared one bucket. Read it with headers.get() instead.
- Models: Organization, OrgMembership (owner/admin/member), Team, TeamMembership (editor/viewer), Project, Environment (versioned, encrypted vars), Invite, ApiToken, AuditLog. User gains 2FA, session-version and last-org fields. Workspace and Group are kept only for the migration. - lib/authz: one place for access checks (manage / edit / view); people outside an org or team get 404, not 403. - lib/auth: session or personal-access-token auth, with 2FA required. - lib/audit: records changes (key names only, never values). - lib/pagination: keyset cursor pagination shared by every list endpoint. - lib/http, validation, slug, dotenv helpers. mongoose sanitizeFilter is on.
- Password sign-up is back alongside Google. Every account must enrol an authenticator app (TOTP) and confirm a code at each sign-in; 10 one-time backup codes; replayed codes are rejected. - Codes are verified server-side and exchanged for a short-lived ticket that the session consumes, so the client never marks itself verified. - Session version: changing the password, resetting 2FA or "sign out everywhere" revokes every existing session. - Middleware sends people to 2FA setup or verification before the app. - Account endpoints: profile, password change, backup codes, sessions. - Auth screens share a split layout: brand panel on the left, the step on the right; Google profile pictures are used as avatars.
- Orgs: create, rename, delete (owner only), transfer ownership. - Members: change roles, remove, reset a member's 2FA (owner only); admins can't touch the owner or other admins. Searchable with ?q=. - Teams and team members (editor/viewer per team); projects; environments (development, staging and production to start), with optimistic concurrency on variable saves (409 when someone saved first). - Audit log (owners and admins): who changed what, never the values. - Personal access tokens and the v1 fetch endpoint for the CLI and CI (JSON or ?format=dotenv). Tokens follow their owner's current access. - Every list endpoint is cursor-paginated (?limit, ?cursor -> nextCursor). - The old group-token endpoint returns 410; workspace and group routes are removed.
- Invitations are 10-character codes (XXXXX-XXXXX, no look-alike characters), also shareable as an /invite/<code> link. Each can add the person to a team with editor or viewer access, expires within 30 days and can be limited to a number of uses. Envoy doesn't send email. - Accepting is atomic, so a limited code can't be over-redeemed, and is rate-limited per user. Codes are matched in any case, with or without the dash. - Public invite preview for the join page. - Onboarding: create an organization (name suggested from your first name) or join with a code; people who already have an org can cancel back.
- One-off .env sharing without an account moves to /share: values are encrypted, and every link expires after 10 minutes, 1 hour or 24 hours at most. Expired links are refused and deleted when opened. - Nothing can be made public any more; old /group?token= URLs redirect.
- Sidebar with org switcher and teams/projects tree, top bar with breadcrumbs for every route, and a Cmd+K command palette. - Every page shares one full-width frame and header. - Org overview with a setup checklist (team, project, invite) based on real progress. - Team page with Projects and Members tabs, each paginated. - Project page: environment tabs and a spreadsheet-style variable editor with masked values, filter, .env import/export, copy for the CLI, unsaved changes bar (Cmd+S), and a dialog before discarding edits. - Settings: general, members (role cards, search), invites and audit log. - Account: profile, password, backup codes, sessions; API tokens page. - Reusable QuickCreateDialog for creating and renaming things. - /dashboard sends you to your last organization, or to onboarding.
- Hero over a pastel gradient with layered frosted-glass panes that react to the pointer, and a headline that reveals word by word. - "Works with your whole stack" grid with real logos, a statement that lights up as you scroll, a feature accordion and use-case tiles. - Floating rounded header; descriptions bold their key phrases. - Respects reduced motion; light and dark themes; works down to phone width.
`npm run migrate:orgs` (supports --dry-run and is safe to re-run): - Each user with workspaces gets an organization they own. - Workspaces become teams; groups become projects with their variables in the development environment, re-encrypted with AES-256-GCM. - Groups with members become their own team so access stays exactly as it was; members join as viewers. - Public flags and group tokens are dropped (the report lists them), and share links older than 24 hours are deleted.
Covers the org/team/project model and roles, mandatory two-factor, invite codes, CLI access with personal tokens, pagination, temporary links, upgrade and migration steps, and breaking changes.
Organizations, teams and projects with mandatory 2FA and a redesigned UI
- Load rate-limiter-flexible with Node at runtime (serverExternalPackages)
instead of bundling it; bundling tried to resolve optional backends such as
drizzle-orm that Envoy doesn't use ("Module not found: drizzle-orm").
- Upgrade to ESLint 9 and eslint-config-next 15.4.4 to match the flat
eslint.config.mjs. With ESLint 8, next build passed it legacy options and
linting failed with "Unknown options: useEslintrc, extensions".
Connect with autoIndex and autoCreate off during --dry-run, so a dry run against a real database writes nothing at all.
Vercel refused to deploy Next.js 15.4.4 ("Vulnerable version of Next.js
detected"). Upgrades within the current major versions:
- next 15.5.29 (latest security backport for 15.x) and eslint-config-next
- next-auth 4.24.15: getToken() no longer crashes on malformed Bearer
headers (API tokens use that header); email normalisation fix
- mongoose 8.24.8: sanitizeFilter now also covers $nor (NoSQL injection)
- patch-level fixes for sharp, preact, uuid and picomatch
Remaining audit notes need major upgrades (postcss inside Next.js 16, the
shadcn CLI) and don't affect the running app.
The repository moved to github.com/GDGVIT/envoy, and the club is GDG VIT (formerly GDSC VIT).
- New footer on the landing and temporary-link pages: an inset green panel with product and project links, "Star it on GitHub" with a note that contributions are welcome, and the Envoy name with the GDG VIT credit. - The header and footer link to github.com/GDGVIT/envoy, and the club is credited as GDG VIT.
The right column was offset downwards, so tiles and captions didn't line up across rows.
- The confirm dialog shows the exact text to type in a highlighted chip
("To confirm, type backend below") and keeps the action disabled until it
matches exactly.
- Every deletion now asks for it: organization (delete or leave), team
(now even with no projects), project and environment by slug; removing a
member from the org or a team, and resetting someone's 2FA, by their email;
API tokens by name.
- Revoking an invite had no confirmation at all; it now asks you to type
"revoke".
…e-run Production values were encrypted with a NEXT_PUBLIC_ENCRYPTION_SECRET that differed from the local one, so the first run skipped them. - Try ENCRYPTION_SECRET, then LEGACY_ENCRYPTION_SECRETS (comma-separated), then the old app's built-in default; reject results that aren't text. - Dry runs now decrypt every value, so they show exactly what would fail. - Re-running refills migrated development environments that nobody has saved since (version 0) from their source group; edited ones are listed and left alone.
- A 404 page for unknown URLs and an error page for unexpected crashes, in the landing page's style, with "Try again", "Go home" and a reference to quote. A last-resort page covers failures in the root layout. - Inside the app, a crash keeps the sidebar and shows a retryable error. - Org, team and project pages no longer say "not found" for every failure: 404s show "not found", anything else shows "Couldn't load this page" with Try again. - Unknown /api paths answer with a JSON 404 like the rest of the API.
Load failures, not-found states and the full-page 404/error screens now sit in the middle of the page with centred text and actions.
Both forms offer the usual presets plus "Custom date…", which expires at the end of the chosen day. The API checks the date is in the future and within the limit (1 year for tokens, 31 days for invites).
Environment reads no longer send values to the browser until the user confirms a fresh TOTP code. The unlock is stored on the account in the database for 10 minutes, never in the browser, and is cleared on sign-out, password change, signing out other sessions and 2FA reset. - POST/GET/DELETE /api/2fa/reveal: unlock (replay-guarded, rate-limited), status, lock - Saving while locked keeps hidden values server-side (rows sent without a value, with "from" for renames) - v1 fetch from a browser session needs the unlock; personal access tokens are unaffected - Editor: masked cells, reveal, copy and download ask for the code, then carry on; "Values unlocked · N min left" with a Lock button; re-locks when time runs out
- Every dialog ends in the same bordered bar: Cancel with an Esc hint and the main action with an Enter hint (also visible when disabled); Enter really submits - The new API token screen copies the token before closing on Enter, since it's only shown once - Selects open below their trigger instead of over it - Side-by-side buttons (invite code/link copy) use the button group
…y've expired - The KEY | VALUE table, value cells and .env import moved to a shared component used by the env editor and both share pages - Create page: Add variable / Import .env toolbar above the table, duplicate and invalid keys flagged; pages line up with the header like the app - Expired links answer 410 and are deleted on access; tokens carry their expiry so a link already cleaned up still reads as expired. Not found and connection errors get their own states - Env editor: the unsaved-changes bar is pinned to the bottom of the window with matching corners, and the key filter can be pre-filled
- New /api/orgs/[org]/search finds environments and variable keys in the teams you can see (names only, never values) - Menu shows recently updated projects, teams, pages, actions and other organizations; substring matching instead of fuzzy; first result always selected - Opening a variable goes to its environment with the key filter set (?key=) - Project header: Use from CLI and the options menu are one button group
- Passkeys (WebAuthn) as a second factor: offered first at setup, used at sign-in, to unlock secret values and to generate backup codes; add, rename and remove them in Account settings - Only public keys are stored; challenges are single use and expire after 5 minutes; attempts are rate-limited - An enrolled account can't add a new passkey or authenticator app from a password-only session - Two-factor is now optional per account. Anyone who has a passkey or app always uses it; people without one see values directly - Organization settings → Security: owner-only 'Require two-factor authentication'. Existing orgs count as on, new orgs start off. Members without 2FA are blocked from a requiring org until they set it up (they can still leave); turning it off resets nothing - The last passkey or app can be removed only when no org of yours requires 2FA; an authenticator app can now be removed too - Owner 2FA reset also removes passkeys; the members list shows Needs 2FA / No 2FA
- The sign-up page only offers Google, so every address is verified. Password sign-up stays available for local development and tests (ALLOW_PASSWORD_SIGNUP) - Account settings → Password: set a first password without a current one (stays signed in), or change it (signs out everywhere). Email + password sign-in then works - Account settings hosts the new two-factor section
…ams, searchable people pickers - A project can be set to 'Only selected people': only the chosen team members (plus org owners and admins) can see it anywhere, including lists, search, CLI and API. New team members don't get access automatically, and removal from the team or org clears them from the list - Move a project, with its environments and values, to another team; refused if that team already has the same project URL; restricted projects keep only people on the new team - Add team member and Manage access use a searchable people list (name or email, arrow keys and Enter) - Both changes are recorded in the audit log
- Drop the Group and Workspace models, the one-time migration script (npm run migrate:orgs) and the tsx dev dependency it used - Drop the leftover workspaces/otherGroups user fields and migratedFrom markers - README: replace the upgrade steps with a short list of breaking changes, and describe Google sign-in, passkeys, optional two-factor, restricted and movable projects, and what ENCRYPTION_SECRET protects
- Switch: the thumb sits 2px inside the track in both positions - Organization settings → Security: switch beside the title, and a footer listing members without two-factor (avatars, names, See members), or a note that everyone has it - Custom expiry dates for API tokens and invite links use the shadcn Calendar in a Popover (adds react-day-picker and date-fns)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Envoy moves from single-owner Workspaces → Groups to Organizations → Teams → Projects → Environments, with roles, optional two-factor (passkeys or authenticator apps), an audit log, personal API tokens and a redesigned UI.
Features
Organizations, teams and projects: org owners and admins create teams and decide who can edit and who can only read.
Multiple environments per project: development, staging and production out of the box; teams can add their own.
Sign in with Google: accounts are created with Google. People can add a password in Account settings to sign in with email too.
Passkeys and authenticator apps: an optional second step at sign-in, using Touch ID, Face ID, Windows Hello, a password manager, or 6-digit codes, with one-time backup codes.
Values stay hidden until you confirm: people with two-factor confirm with a passkey or code before secret values are sent to the browser. The unlock lasts 10 minutes and is stored on the server, not in the browser.
Restricted projects: a project can be limited to selected team members, and projects can be moved between teams.
Invitations: 10-character invite codes, also shareable as a link, that expire and can be limited to a number of uses. Envoy doesn't send email.
Audit log: owners and admins see every change to members, teams, projects and variables. It records key names only, never values.
Personal API tokens: fetch variables from the CLI or CI with
/api/v1/…. Tokens follow the user's current access.Encrypted at rest: AES-256-GCM with a server-only key, decrypted only for people with access.
Safe concurrent editing: saves are versioned, so two editors can't silently overwrite each other.
Temporary links:
.envonce without an account; links expire within 24 hours and are then deleted.Redesigned UI:
Rate limiting: built-in limits per IP and per account.
Breaking changes
GET /api/env/getreturns410 Gone; use personal API tokens and/api/v1/….XXXXX-XXXXX), also usable as an/invite/<code>link.Deploying
ENCRYPTION_SECRET(server only) replacesNEXT_PUBLIC_ENCRYPTION_SECRET.NEXT_PUBLIC_ENCRYPTION_SECRETvalue.WEBAUTHN_RP_IDandWEBAUTHN_ORIGINpin passkeys to one domain.npm run migrate:orgs -- --dry-run, thennpm run migrate:orgs, from commit 3f45e1a first.Testing
tsc,eslintandnext buildare clean.