Inspiration

"Secure Notes for Jira" was inspired by a common challenge faced by many teams: the need to share sensitive information — such as access credentials, API keys, private feedback, or temporary passwords — directly within a Jira issue, without exposing it in issue fields, comments, or descriptions. While Jira excels at task tracking and collaboration, it lacks a secure, ephemeral channel for confidential communication.

Through hands-on experience working with Forge, I encountered this limitation firsthand. I often needed a secure mechanism for sharing notes that are confidential, time-limited, and verifiably read by the intended recipient — with automatic deletion immediately after viewing. A major gap in existing solutions is that they separate the sensitive communication from the context of the Jira issue itself. When sensitive data is exchanged outside of Jira (e.g., via email, chat, or tools like 1Password and LastPass), that connection to the original task — and its purpose — is lost. What’s truly needed is a solution that allows sharing confidential information directly within the relevant issue, ensuring it remains tied to its operational context.

This also unlocks future potential: by storing metadata securely, the app can later generate audit reports showing which users accessed sensitive data, when, and in connection to which issues. This would help organizations understand access flows, data exposure points, and operational intent — all without ever storing the sensitive content in plaintext.

To address this, I set out to create Secure Notes for Jira — a secure, ephemeral notes app that lives entirely within Atlassian Forge, with no external backend, no persistent plaintext storage, and a strong link between sensitive communication and its Jira issue context, ensuring no compromise on confidentiality.

What It Does

🔒 Security Features

  • 🔐 Create encrypted notes on Jira issues

  • 🕒 Choose an expiration: 1 hour, 1 day, 7 days, 10 days

  • 🔑 Generate a one-time decryption key (not stored)

  • 📥 View received notes (with key)

  • 📤 View and delete sent notes

  • 🧨 Note self-destructs after reading or upon expiry

  • ⏳ Expiration is enforced automatically using a Forge scheduledTrigger that runs every 5 minutes

  • 👤 Only the designated Atlassian account can decrypt the Secure Note — others will see a 404 or access-denied page

🖥 UI Features

  • 📎 Open decryption links directly from the Issue Panel or via email
  • 🧭 Support for routing and deep-linking to global pages with Secure Note details
  • ⏱️ 5-minute countdown timer shown during note viewing — after which the note is closed and cannot be accessed again
  • 🌓 Full dark/light mode support based on Jira theme

How I Built It

  • Frontend: React + Vite (Forge Custom UI)
  • Backend: Forge Functions using @forge/api, @forge/sql, @forge/kvs
  • ORM: forge-sql-orm — my custom library based on Drizzle
  • Storage: Encrypted note content is stored in @forge/kvs (via setSecret), while all metadata (including expiry, IV, salt, key hash) resides in @forge/sql

Security Design

  • Encryption is handled entirely on the client using the Web Crypto API
  • AES-GCM is used with a 32-byte key securely derived from the user-provided shared secret via a multi-step PBKDF2 process.
  • IV is randomly generated for each message and stored/transmitted alongside the ciphertext
  • Encrypted note content is stored using @forge/kvs.setSecret, ensuring it is kept private even from the backend
  • All metadata about the note — such as expiry, sender, recipient, status, IV, salt — is stored separately in @forge/sql
  • Encrypted data is stored in HEX format (as returned by bufferToHex) in Forge KVS
  • The decryption key is never sent to the backend — it must be shared out-of-band (e.g., Slack, Signal)
  • After the note is viewed, the encrypted content is deleted from KVS; metadata in SQL is retained and marked as 'viewed' or 'deleted' for audit purposes only
Encrypting Flow
  • When a user creates a new Secure Note, they select a recipient and set an expiration time.
  • The plaintext message is encrypted on the client using AES-GCM.
  • The encryption key is derived from a passphrase using a two-step PBKDF2-like process:

    • First: baseKey = hash(userInput, userAccountId, 200_000 iterations)
    • Then: two keys are derived:
    • encryptionKey for encrypting content
    • serverKey for verifying the decryption key later
  • The encrypted result includes the ciphertext, IV, and salt.

  • The ciphertext is saved via @forge/kvs.setSecret, while metadata such as IV, salt, expiry, sender, recipient, and hash(serverKey) is saved in @forge/sql.

  • A unique decryption link is generated and sent to the recipient via email.

  • The secret key is never stored — it is shown only once and must be shared securely out-of-band.

  baseKey = hash(userProvidedKey, userAccountId, 200_000)
  encryptionKey = hash(baseKey, 'ENCRYPTION', 1000)
  serverKey = hash(baseKey, 'VERIFICATION', 1000)
  encrypted = encrypt(note, encryptionKey)

  kvs.setSecret(noteId, encrypted)
  sql.insert({
    encryptionKeyHash: hash(serverKey, userAccountId),
    ...metadata
  })
  sendLinkToRecipient(noteId)
  • Expiration enforcement is handled automatically by an Atlassian Forge scheduledTrigger, which runs every 5 minutes. This trigger retrieves up to 50 expired notes at a time, deletes their encrypted content from @forge/kvs, updates their status to expired in @forge/sql, and sends a notification to the recipient informing them that the note is no longer available.
Decryption Flow
  • When a Secure Note is created, a unique decryption link is generated. This link is immediately available inside the Issue Panel of the Jira issue where the note was created — but is only visible to the intended recipient.
  • Additionally, the same link is sent to the recipient via email for convenience.
  • This link opens a Forge global page where the recipient can decrypt the message.
  • When the recipient opens the link, a Forge resolver verifies whether their account ID matches the one assigned to the note's recipient. If not, a 404 page is displayed.
  • If the user is authorized, a prompt asks the recipient to enter the secret key, which must have been shared out-of-band (e.g., via Telegram or Slack)..
  • Upon key submission, the client derives a verification key by reapplying the same key derivation steps:
baseKey = hash(userInput, accountId, 200_000)
keyForServer = hash(baseKey, 'VERIFICATION', 1000)
  • This keyForServer is then sent to the backend for validation.

  • The backend verifies this hash against the stored hash in @forge/sql. If it doesn't match, an error is returned.

  • If the hash is valid, the encrypted content is retrieved from @forge/kvs, returned to the client, and immediately deleted via kvs.deleteSecret.

  • The associated metadata in @forge/sql is updated to mark the note as viewed, allowing it to be retained solely for auditing purposes.

const sn = await getSecurityNote(id);
if (sn.encryptionKeyHash !== await hash(keyForServer, accountId)) {
  throw new Error('Invalid key');
}
const encrypted = await kvs.getSecret(id);
await kvs.deleteSecret(id);
await markAsViewed(id);
return encrypted;

`

Accomplishments

  • Created a unified, production-grade secure messaging app inside Jira using Atlassian Forge — with full client-side encryption, strict access control, and automatic cleanup. Sensitive content is never stored in plaintext or exposed to the server.

  • Built a reusable Custom UI frontend (React + Vite) that works across multiple Forge contexts — Issue Panel, Modal Dialogs, and Global Page — with features like theme support and client-side routing.

Challenges

  • Integrating multiple Forge modules (Issue Panel, Global Page, Functions, SQL, KVS) into a unified and secure flow — required precise coordination of contexts and data.
  • Designing an intuitive experience for a security-focused app — balancing clarity, encryption responsibility, and one-time access behavior.
  • Handling modal dialogs inside the Issue Panel — ensuring smooth UX with correct state flow.
  • Building a reusable frontend (React + Vite) across three Forge contexts — Issue Panel, Modal Dialogs, and Global Page — while keeping design consistent and code maintainable. This also included implementing React routing support for Global Page views tied to specific Secure Note links.
  • Enforcing strong security entirely on the frontend — with ephemeral keys and no backend access to plaintext.

What I Learned

  • I learned how to build a complex, production-grade Forge app that qualifies for the Runs on Atlassian program — using only Atlassian infrastructure, with no external services or backend.

  • I learned how to organize and maintain a single React + Vite frontend across three Forge contexts: Issue Panel, Modal Dialog, and Global Page. This included using runtime context to decide what UI to render.

  • I learned how to implement client-side routing in the Global Page so users can view secure notes via shareable links — like /view/:noteId — and route them to the correct screen.

  • I learned how to work with modal dialogs inside an Issue Panel — handling user input, view state, and form behavior cleanly.

  • I learned how to use the Jira REST API via Forge’s api.asApp().requestJira() to send email notifications (e.g. note shared, expired, deleted) without leaving a trace in issue comments.

What I’m Excited About Building with Forge Moving Forward

  • Audit Log Page: Add a global page showing notes sent, received, and viewed. Admins could see audit history tied to users or issues — based on metadata already in @forge/sql.
  • Secure Note Dashboard: Let users see all active notes (sent or received) across all issues from one place.
  • Custom Notifications: Allow users or admins to configure which note events trigger emails (e.g., shared, expired, read).
  • Encrypted Attachments: Add support for uploading and encrypting small files (~500KB), stored securely using @forge/kvs or Forge Object Storage.
  • Note Titles or Comments: Let senders include an optional unencrypted note title or context string — stored in @forge/sql.
  • Marketplace Readiness: Improve docs, polish UX, and meet all security and performance guidelines for publishing to Atlassian Marketplace.

Built With

Share this project:

Updates