Skip to content

Organizations, passkeys and a redesigned Envoy - #4

Merged
ChirayuSahu merged 32 commits into
GDGVIT:devfrom
ChirayuSahu:dev
Nov 12, 2026
Merged

ChirayuSahu merged 32 commits into
GDGVIT:devfrom
ChirayuSahu:dev

Conversation

@ChirayuSahu

Copy link
Copy Markdown
Contributor

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.

    Role Can do
    Org owner Everything, plus delete the org, transfer ownership, require two-factor, reset a member's 2FA
    Org admin Create teams, invite people, change roles, edit every project, view the audit log
    Org member See only the teams they're on
    Team editor Read and change variables, create projects and environments in that team
    Team viewer Read variables only
  • 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.

    • Owners can require two-factor for their organization.
    • Turning the requirement off never removes anyone's passkeys or apps.
  • 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:

    • Share a .env once without an account; links expire within 24 hours and are then deleted.
    • Expired links say so, and environments are never public.
  • Redesigned UI:

    • shadcn/ui on Tailwind v4, with dark and light themes and a new landing page.
    • A spreadsheet-style variables editor.
    • A ⌘K command menu that searches projects, teams, environments and variable names.
    • Keyboard shortcuts, and responsive down to mobile.
  • Rate limiting: built-in limits per IP and per account.

Breaking changes

  • Group tokens no longer work. GET /api/env/get returns 410 Gone; use personal API tokens and /api/v1/….
  • Nothing can be made public. Use invite links, or temporary links (at most 24 hours) for one-off sharing.
  • Invitations are codes now. Each is a 10-character code (XXXXX-XXXXX), also usable as an /invite/<code> link.
  • Sign-up is Google only. People can add a password afterwards.

Deploying

  • ENCRYPTION_SECRET (server only) replaces NEXT_PUBLIC_ENCRYPTION_SECRET.
    • It encrypts variable values, temporary links and authenticator-app secrets.
    • Changing it makes that data unreadable, so keep it backed up.
    • For existing data, set it to the old NEXT_PUBLIC_ENCRYPTION_SECRET value.
  • Optional: WEBAUTHN_RP_ID and WEBAUTHN_ORIGIN pin passkeys to one domain.
  • Existing Workspaces/Groups data must be migrated before deploying.
    • The migration script was added in 4346595 and removed after use in 8796d93.
    • If this deployment's database still has old data, run npm run migrate:orgs -- --dry-run, then npm run migrate:orgs, from commit 3f45e1a first.

Testing

  • API end-to-end checks: 272 checks, all passing, against a local in-memory MongoDB:
    • auth and TOTP
    • passkeys, using a software WebAuthn authenticator
    • the org two-factor requirement and passwords
    • restricted projects
    • moving projects
  • Build: tsc, eslint and next build are clean.
  • Manual: the main screens were checked in the browser, in light and dark themes.

ChirayuSahu and others added 30 commits November 7, 2026 14:55
- 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)
@ChirayuSahu
ChirayuSahu merged commit 281508a into GDGVIT:dev Nov 12, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant