feat: iso27001 certificate (#44963)

Edit: Can be merged, mgmt api deployed

Dashboard addition to frontend for access to the ISO 27001 certificate.

View for Team customers:
<img width="1737" height="1151" alt="image"
src="https://github.com/user-attachments/assets/cd62d24f-8b6e-4600-9ded-943a170cd124"
/>

Resolves SEC-799

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* ISO 27001 certificate added to Documents with a Download action,
confirmation modal, new-tab open on success, and error toast on failure.
* Users without billing permission see a no-permission view; users
missing entitlement see an “Upgrade to Team” prompt.

* **Refactor**
* Upgrade-to-Team flows for SOC2 and related upgrade UI standardized to
use the shared upgrade component.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Stephen MorganandJoshen Lim authored and GitHub committed 2026-04-21 16:38:03 +12:00
1 parent 6b8772a33e
commit bd55ad23a6
9 files changed
+248 -16

No files matched your search

@@ -3,6 +3,7 @@ import { Fragment } from 'react'
import { CustomDocument } from './CustomDocument'
import { DPA } from './DPA'
import { HIPAA } from './HIPAA'
import { ISO27001 } from './ISO27001'
import { SecurityQuestionnaire } from './SecurityQuestionnaire'
import { SOC2 } from './SOC2'
import { TIA } from './TIA'
@@ -50,6 +51,12 @@ export const Documents = () => {
<ScaffoldDivider />
<ScaffoldContainer id="iso27001" className="px-6 xl:px-10">
<ISO27001 />
</ScaffoldContainer>
<ScaffoldDivider />
<ScaffoldContainer id="hipaa" className="px-6 xl:px-10">
<HIPAA />
</ScaffoldContainer>
@@ -0,0 +1,145 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Download } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import {
ScaffoldSection,
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from '@/components/layouts/Scaffold'
import NoPermission from '@/components/ui/NoPermission'
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
import { getDocument } from '@/data/documents/document-query'
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
export const ISO27001 = () => {
const { data: organization } = useSelectedOrganizationQuery()
const slug = organization?.slug
const { mutate: sendEvent } = useSendEventMutation()
const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.BILLING_READ,
'stripe.subscriptions'
)
const { hasAccess: hasAccessToISO27001, isLoading: isLoadingEntitlement } = useCheckEntitlements(
'security.iso27001_certificate'
)
const [isOpen, setIsOpen] = useState(false)
const fetchISO27001 = async (orgSlug: string) => {
try {
const link = await getDocument({ orgSlug, docType: 'iso27001-certificate' })
if (link?.fileUrl) window.open(link.fileUrl, '_blank')
setIsOpen(false)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error occurred'
toast.error(`Failed to download ISO 27001 certificate: ${message}`)
}
}
const handleDownloadClick = () => {
if (!slug) return
sendEvent({
action: 'document_view_button_clicked',
properties: { documentName: 'ISO27001' },
groups: { organization: slug },
})
setIsOpen(true)
}
return (
<ScaffoldSection className="py-12">
<ScaffoldSectionDetail>
<h4 className="mb-5">ISO 27001</h4>
<div className="space-y-2 text-sm text-foreground-light [&_p]:m-0">
<p>
Organizations on Team Plan or above have access to our most recent ISO 27001
certificate.
</p>
</div>
</ScaffoldSectionDetail>
<ScaffoldSectionContent>
{isLoadingPermissions || isLoadingEntitlement ? (
<div className="@lg:flex items-center justify-center h-full">
<ShimmeringLoader className="w-24" />
</div>
) : !canReadSubscriptions ? (
<NoPermission resourceText="access our ISO 27001 certificate" />
) : !hasAccessToISO27001 ? (
<div className="@lg:flex items-center justify-center h-full">
<UpgradePlanButton
variant="default"
plan="Team"
source="org-documents-iso27001"
featureProposition="download the ISO 27001 certificate"
/>
</div>
) : (
<div className="@lg:flex items-center justify-center h-full">
<Button
type="default"
icon={<Download />}
onClick={handleDownloadClick}
disabled={!slug}
>
Download ISO 27001 Certificate
</Button>
</div>
)}
<ConfirmationModal
visible={isOpen}
size="large"
title="Non-Disclosure Agreement to access Supabase's ISO 27001 Certificate"
confirmLabel="I agree"
confirmLabelLoading="Downloading"
onCancel={() => setIsOpen(false)}
onConfirm={() => {
if (slug) fetchISO27001(slug)
}}
>
<ol className="list-decimal list-inside text-sm text-foreground-light pl-30">
<li>The information that you are about to access is confidential.</li>
<li>
Your access to our ISO 27001 materials is governed by confidentiality obligations
contained in the agreement between Supabase, Inc ("Supabase", "we", "our" or "us") and
the Supabase customer that has authorized you to access our platform to obtain this
information (our "Customer").
</li>
<li>
You must ensure that you treat the information in our ISO 27001 materials in
accordance with those confidentiality obligations, as communicated to you by the
Customer.
</li>
<li>
By clicking "I agree" below or otherwise accessing our ISO 27001 materials, you:
<ol className="list-[lower-roman] list-inside pl-4">
<li>acknowledge that you have read and understood this Confidentiality Notice;</li>
<li>
confirm that you have been authorized by the Customer to access this information,
and your use of our ISO 27001 materials is subject to the confidentiality
obligations owed by the Customer to us.
</li>
</ol>
</li>
<li>
This Confidentiality Notice does not substitute or supersede any agreement between us
and the Customer, or any internal rules or policies that the Customer requires you to
comply with in your access to and use of confidential information. However, your
failure to comply with this Confidentiality Notice may be used to determine whether
the Customer has complied with its confidentiality obligations to us.
</li>
</ol>
</ConfirmationModal>
</ScaffoldSectionContent>
</ScaffoldSection>
)
}
@@ -1,6 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Download } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from 'ui'
@@ -13,6 +12,7 @@ import {
ScaffoldSectionDetail,
} from '@/components/layouts/Scaffold'
import NoPermission from '@/components/ui/NoPermission'
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
import { getDocument } from '@/data/documents/document-query'
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
@@ -74,11 +74,12 @@ export const SOC2 = () => {
<NoPermission resourceText="access our SOC2 Type 2 report" />
) : !hasAccessToSoc2Report ? (
<div className="@lg:flex items-center justify-center h-full">
<Button asChild type="default">
<Link href={`/org/${slug}/billing?panel=subscriptionPlan&source=soc2`}>
Upgrade to Team
</Link>
</Button>
<UpgradePlanButton
variant="default"
plan="Team"
source="org-documents-soc2"
featureProposition="download the SOC2 Type 2report"
/>
</div>
) : (
<div className="@lg:flex items-center justify-center h-full">
@@ -48,6 +48,7 @@ interface RequestUpgradeToBillingOwnersProps {
/** Used in the default message template, e.g: "Upgrade to ..." */
featureProposition?: string
className?: string
type?: 'primary' | 'default'
}
export const RequestUpgradeToBillingOwners = ({
@@ -57,6 +58,7 @@ export const RequestUpgradeToBillingOwners = ({
featureProposition,
children,
className,
type = 'primary',
}: PropsWithChildren<RequestUpgradeToBillingOwnersProps>) => {
const [open, setOpen] = useState(false)
const track = useTrack()
@@ -121,10 +123,8 @@ export const RequestUpgradeToBillingOwners = ({
const defaultValues = {
note: !!addon
? addon === 'spendCap'
? `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
: `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
: `We'd like to upgrade to the ${plan} plan ${!!featureProposition ? ` to ${featureProposition} ` : ''}${target}`,
? `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
: `We'd like to upgrade to the ${plan} plan ${!!featureProposition ? `to ${featureProposition} ` : ''}${target}`,
}
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
@@ -162,7 +162,7 @@ export const RequestUpgradeToBillingOwners = ({
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button block={block} type="primary" className={className}>
<Button block={block} type={type} className={className}>
{buttonText}
</Button>
</DialogTrigger>
@@ -35,7 +35,7 @@ interface UpgradePlanButtonProps {
*/
export const UpgradePlanButton = ({
source,
variant = 'primary',
variant: type = 'primary',
plan = 'Pro',
addon,
featureProposition,
@@ -97,6 +97,7 @@ export const UpgradePlanButton = ({
addon={addon}
featureProposition={featureProposition}
className={className}
type={type}
>
{children}
</RequestUpgradeToBillingOwners>
@@ -107,7 +108,7 @@ export const UpgradePlanButton = ({
return (
<ButtonTooltip
disabled
type={variant}
type={type}
className={className}
tooltip={{
content: {
@@ -122,7 +123,7 @@ export const UpgradePlanButton = ({
}
return (
<Button asChild type={variant} disabled={disabled} className={className} onClick={onClick}>
<Button asChild type={type} disabled={disabled} className={className} onClick={onClick}>
{link}
</Button>
)
+17 -1
View File
@@ -4,7 +4,10 @@ import { documentKeys } from './keys'
import { get, handleError } from '@/data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type DocType = 'standard-security-questionnaire' | 'soc2-type-2-report'
export type DocType =
| 'standard-security-questionnaire'
| 'soc2-type-2-report'
| 'iso27001-certificate'
export type DocumentVariables = {
orgSlug?: string
@@ -39,6 +42,19 @@ export async function getDocument({ orgSlug, docType }: DocumentVariables, signa
return data as { fileUrl: string }
}
if (docType === 'iso27001-certificate') {
const { data, error } = await get(
`/platform/organizations/{slug}/documents/iso27001-certificate`,
{
params: { path: { slug: orgSlug } },
signal,
}
)
if (error) throw error
return data as { fileUrl: string }
}
}
export type DocumentData = Awaited<ReturnType<typeof getDocument>>
+1
View File
@@ -4896,6 +4896,7 @@ export interface components {
| 'security.audit_logs_days'
| 'security.questionnaire'
| 'security.soc2_report'
| 'security.iso27001_certificate'
| 'security.private_link'
| 'security.enforce_mfa'
| 'log.retention_days'
+61
View File
@@ -1372,6 +1372,23 @@ export interface paths {
patch?: never
trace?: never
}
'/platform/organizations/{slug}/documents/iso27001-certificate': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** Get ISO 27001 certificate URL */
get: operations['OrgDocumentsController_getIso27001CertificateUrl']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/platform/organizations/{slug}/documents/soc2-type-2-report': {
parameters: {
query?: never
@@ -7263,6 +7280,7 @@ export interface components {
| 'security.audit_logs_days'
| 'security.questionnaire'
| 'security.soc2_report'
| 'security.iso27001_certificate'
| 'security.private_link'
| 'security.enforce_mfa'
| 'log.retention_days'
@@ -15414,6 +15432,49 @@ export interface operations {
}
}
}
OrgDocumentsController_getIso27001CertificateUrl: {
parameters: {
query?: never
header?: never
path: {
/** @description Organization slug */
slug: string
}
cookie?: never
}
requestBody?: never
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['OrgDocumentUrlResponse']
}
}
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Forbidden action */
403: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Rate limit exceeded */
429: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
OrgDocumentsController_getSoc2Type2ReportUrl: {
parameters: {
query?: never
+1 -1
View File
@@ -2016,7 +2016,7 @@ export interface DocumentViewButtonClickedEvent {
/**
* The name of the document being viewed, e.g. TIA, SOC2, Standard Security Questionnaire
*/
documentName: 'TIA' | 'SOC2' | 'Standard Security Questionnaire'
documentName: 'TIA' | 'SOC2' | 'ISO27001' | 'Standard Security Questionnaire'
}
groups: Omit<TelemetryGroups, 'project'>
}