diff --git a/apps/docs/content/guides/functions/secrets.mdx b/apps/docs/content/guides/functions/secrets.mdx index c3a4f96c73f..18abb88c59b 100644 --- a/apps/docs/content/guides/functions/secrets.mdx +++ b/apps/docs/content/guides/functions/secrets.mdx @@ -1,11 +1,13 @@ --- id: 'functions-secrets' -title: 'Managing Environment Variables' +title: 'Managing Secrets (Environment Variables)' description: 'Managing secrets and environment variables.' subtitle: 'Managing secrets and environment variables.' --- -It's common that you will need to use sensitive information or environment-specific variables inside your Edge Functions. You can access these using Deno's built-in handler +It's common that you will need to use environment variables or other sensitive information Edge Functions. You can manage secrets using the CLI or the Dashboard. + +You can access these using Deno's built-in handler ```js Deno.env.get('MY_SECRET_NAME') @@ -57,7 +59,25 @@ When the function starts you should see the name “Yoda” output to the termin ## Production secrets -Let's create a `.env` for production. In this case we'll just use the same as our local secrets: +You will also need to set secrets for your production Edge Functions. You can do this via the Dashboard or using the CLI. + +### Using the Dashboard + +1. Visit [Edge Function Secrets Management](https://supabase.com/dashboard/project/_/settings/functions) page in your Dashboard. +2. Add the Key and Value for your secret and press Save. +3. Note that you can paste multiple secrets at a time. + +Edge Functions Secrets Management + +### Using the CLI + +Let's create a `.env` to help us deploy our secrets to production. In this case we'll just use the same as our local secrets: ```bash cp ./supabase/.env.local ./supabase/.env @@ -67,7 +87,7 @@ This creates a new file `./supabase/.env` for storing your production secrets. -Never check your `.env` files into Git! +Never check your `.env` files into Git! You only use the `.env` file to help deploy your secrets to production. Don't commit it to your repository. diff --git a/apps/docs/public/img/edge-functions-secrets--light.jpg b/apps/docs/public/img/edge-functions-secrets--light.jpg new file mode 100644 index 00000000000..ca1dffb6b20 Binary files /dev/null and b/apps/docs/public/img/edge-functions-secrets--light.jpg differ diff --git a/apps/docs/public/img/edge-functions-secrets.jpg b/apps/docs/public/img/edge-functions-secrets.jpg new file mode 100644 index 00000000000..fadebfb747c Binary files /dev/null and b/apps/docs/public/img/edge-functions-secrets.jpg differ diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 11f6df3e22c..06071962e59 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -37,7 +37,7 @@ const EdgeFunctionDetails = () => { const router = useRouter() const { ref: projectRef, functionSlug } = useParams() const [showDeleteModal, setShowDeleteModal] = useState(false) - const [showInstructions, setShowInstructions] = useState(false) + const [showInstructions, setShowInstructions] = useState(true) const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) @@ -220,7 +220,7 @@ const EdgeFunctionDetails = () => {
-

Command line access

+

Deploy, invoke and manage secrets

setShowInstructions(!showInstructions)}> {showInstructions ? ( @@ -231,11 +231,11 @@ const EdgeFunctionDetails = () => {
-
Deployment management
+
Deploy your function
-
Invoke
+
Invoke your function
-
Secrets management
+
Manage secrets
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx new file mode 100644 index 00000000000..ba7cc877392 --- /dev/null +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx @@ -0,0 +1,191 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useState } from 'react' +import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import z from 'zod' + +import { useParams } from 'common' +import Panel from 'components/ui/Panel' +import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation' +import { Eye, EyeOff, MinusCircle } from 'lucide-react' +import { + Button, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + FormItem_Shadcn_, + FormLabel_Shadcn_, + FormMessage_Shadcn_, +} from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' + +type SecretPair = { + name: string + value: string +} + +const FormSchema = z.object({ + secrets: z.array( + z.object({ + name: z + .string() + .min(1, 'Please provide a name for your secret') + .refine((value) => !value.match(/^(SUPABASE_).*/), { + message: 'Name must not start with the SUPABASE_ prefix', + }), + value: z.string().min(1, 'Please provide a value for your secret'), + }) + ), +}) + +const defaultValues = { + secrets: [{ name: '', value: '' }], +} +const AddNewSecretForm = () => { + const { ref: projectRef } = useParams() + const [showSecretValue, setShowSecretValue] = useState(false) + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues, + }) + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: 'secrets', + }) + + function handlePaste(e: ClipboardEvent) { + e.preventDefault() + const text = e.clipboardData?.getData('text') + if (!text) return + + const pairs: Array = [] + + try { + const jsonData = JSON.parse(text) + Object.entries(jsonData).forEach(([key, value]) => { + pairs.push({ name: key, value: String(value) }) + }) + } catch { + // Try KEY=VALUE format (multiple lines) + const lines = text.split(/\n/) + lines.forEach((line) => { + const [key, ...valueParts] = line.split('=') + if (key && valueParts.length) { + pairs.push({ + name: key.trim(), + value: valueParts.join('=').trim(), + }) + } + }) + } + + if (pairs.length) { + // Replace all fields with new pairs + form.reset({ secrets: pairs }) + } + } + + const { mutate: createSecret, isLoading: isCreating } = useSecretsCreateMutation({ + onSuccess: (_, variables) => { + toast.success(`Successfully created new secret "${variables.secrets[0].name}"`) + // RHF recommends using setTimeout/useEffect to reset the form + setTimeout(() => form.reset(), 0) + }, + }) + + const onSubmit: SubmitHandler> = async (data) => { + createSecret({ projectRef, secrets: data.secrets }) + } + + return ( + + +

Add new secrets

+ +
+ {fields.map((fieldItem, index) => ( +
+ ( + + Key + + handlePaste(e.nativeEvent)} + /> + + + + )} + /> + ( + + Value + + +
+ } + /> + + + + )} + /> + + + +
+ +
+ +
+
+
+ ) +} + +export default AddNewSecretForm diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretModal.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretModal.tsx deleted file mode 100644 index e0942c2e73e..00000000000 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretModal.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { useEffect, useRef, useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import z from 'zod' - -import { useParams } from 'common' -import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation' -import { Eye, EyeOff } from 'lucide-react' -import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Modal } from 'ui' -import { Input } from 'ui-patterns/DataInputs/Input' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' - -interface AddNewSecretModalProps { - visible: boolean - onClose: () => void -} - -const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { - const { ref: projectRef } = useParams() - const submitRef = useRef(null) - const [showSecretValue, setShowSecretValue] = useState(false) - - const FormSchema = z.object({ - name: z - .string() - .min(1, 'Please provide a name for your secret') - .refine((value) => !value.match(/^(SUPABASE_).*/), { - message: 'Name must not start with the SUPABASE_ prefix', - }), - value: z.string().min(1, 'Please provider a value for your secret'), - }) - const defaultValues = { name: '', value: '' } - - const form = useForm({ - resolver: zodResolver(FormSchema), - defaultValues, - }) - - const onSubmit: SubmitHandler> = async (data) => { - createSecret({ projectRef, secrets: [data] }) - } - - const { mutate: createSecret, isLoading: isCreating } = useSecretsCreateMutation({ - onSuccess: (_, variables) => { - toast.success(`Successfully created new secret "${variables.secrets[0].name}"`) - onClose() - }, - }) - - useEffect(() => { - if (visible) { - form.reset(defaultValues) - setShowSecretValue(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [visible]) - - return ( - - - - - } - > - - -
- ( - - - - - - )} - /> - ( - - - -