> ## Documentation Index
> Fetch the complete documentation index at: https://notikaai.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# PostgreSQL

> How to connect your Postgres database to Dreamlit.

export const OpenAccordionsOnHash = () => {
  useEffect(() => {
    const normalizeHash = hash => {
      const raw = (hash || "").replace(/^#/, "");
      try {
        return decodeURIComponent(raw);
      } catch {
        return raw;
      }
    };
    const openAccordionAncestors = element => {
      let detailsElement = element?.closest?.("details.accordion") ?? null;
      while (detailsElement) {
        detailsElement.open = true;
        detailsElement = detailsElement.parentElement?.closest?.("details.accordion") ?? null;
      }
    };
    const tryOpenForHash = () => {
      const targetId = normalizeHash(window.location.hash);
      if (!targetId) return;
      let remainingAttempts = 10;
      const attempt = () => {
        const target = document.getElementById(targetId);
        if (target) {
          openAccordionAncestors(target);
          const targetAfterOpen = document.getElementById(targetId);
          targetAfterOpen?.scrollIntoView?.({
            block: "start"
          });
          return;
        }
        remainingAttempts -= 1;
        if (remainingAttempts <= 0) return;
        (window.requestAnimationFrame ?? window.setTimeout)(attempt);
      };
      attempt();
    };
    tryOpenForHash();
    window.addEventListener("hashchange", tryOpenForHash);
    return () => window.removeEventListener("hashchange", tryOpenForHash);
  }, []);
  return null;
};

export const PostgresNewSchemaGrant = () => {
  const [schema, setSchema] = useState("public");
  const [role, setRole] = useState("dreamlit_app");
  const [owner, setOwner] = useState("postgres");
  const [copyLabel, setCopyLabel] = useState("Copy SQL");
  const inputStyle = {
    padding: "6px 8px",
    border: "1px solid #d1d5db",
    borderRadius: 6,
    fontSize: 13,
    width: "100%",
    boxSizing: "border-box"
  };
  const labelStyle = {
    fontSize: 12,
    fontWeight: 600,
    color: "#374151",
    marginBottom: 4
  };
  const quoteIdent = v => `"${v.replace(/"/g, '""')}"`;
  const sql = useMemo(() => {
    const schemaIdent = quoteIdent(schema.trim() || "public");
    const roleIdent = quoteIdent(role.trim() || "dreamlit_app");
    const ownerIdent = quoteIdent(owner.trim() || "postgres");
    return `-- Run as the role that owns/creates tables in this schema (usually ${ownerIdent})
GRANT USAGE ON SCHEMA ${schemaIdent} TO ${roleIdent};
GRANT SELECT, TRIGGER ON ALL TABLES IN SCHEMA ${schemaIdent} TO ${roleIdent};
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${schemaIdent} TO ${roleIdent};
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA ${schemaIdent} TO ${roleIdent};

ALTER DEFAULT PRIVILEGES FOR ROLE ${ownerIdent} IN SCHEMA ${schemaIdent}
  GRANT SELECT, TRIGGER ON TABLES TO ${roleIdent};
ALTER DEFAULT PRIVILEGES FOR ROLE ${ownerIdent} IN SCHEMA ${schemaIdent}
  GRANT USAGE, SELECT ON SEQUENCES TO ${roleIdent};
ALTER DEFAULT PRIVILEGES FOR ROLE ${ownerIdent} IN SCHEMA ${schemaIdent}
  GRANT EXECUTE ON FUNCTIONS TO ${roleIdent};`;
  }, [schema, role, owner]);
  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(sql);
      setCopyLabel("Copied!");
      setTimeout(() => setCopyLabel("Copy SQL"), 1800);
    } catch {
      setCopyLabel("Copy failed");
      setTimeout(() => setCopyLabel("Copy SQL"), 1800);
    }
  };
  return <div style={{
    border: "1px solid #e5e7eb",
    borderRadius: 10,
    padding: 14,
    background: "#fafafa",
    margin: "16px 0"
  }}>
      <div style={{
    display: "grid",
    gridTemplateColumns: "1fr 1fr 1fr",
    gap: 10,
    marginBottom: 10
  }}>
        <div>
          <div style={labelStyle}>Schema</div>
          <input value={schema} onChange={e => setSchema(e.target.value)} placeholder="public" style={inputStyle} />
        </div>
        <div>
          <div style={labelStyle}>Dreamlit role</div>
          <input value={role} onChange={e => setRole(e.target.value)} placeholder="dreamlit_app" style={inputStyle} />
        </div>
        <div>
          <div style={labelStyle}>Table-owner role</div>
          <input value={owner} onChange={e => setOwner(e.target.value)} placeholder="postgres" style={inputStyle} />
        </div>
      </div>

      <div style={{
    marginBottom: 10
  }}>
        <div style={{
    ...labelStyle,
    marginBottom: 6
  }}>SQL</div>
        <pre style={{
    border: "1px solid #d1d5db",
    borderRadius: 8,
    background: "#111827",
    color: "#e5e7eb",
    padding: 12,
    fontSize: 12,
    lineHeight: 1.5,
    overflowX: "auto",
    whiteSpace: "pre-wrap"
  }}>
          {sql}
        </pre>
      </div>

      <button onClick={handleCopy} style={{
    background: "#111827",
    color: "white",
    border: "none",
    borderRadius: 6,
    padding: "8px 12px",
    fontSize: 13,
    cursor: "pointer"
  }}>
        {copyLabel}
      </button>
    </div>;
};

export const PostgresSqlBuilder = () => {
  const TABLE_OWNER_DEFAULT = "postgres";
  const TABLE_OWNER_NEON = "neondb_owner";
  const HOST_HINT_DEFAULT = "db.example.com";
  const HOST_HINT_NEON = "ep-sunny-lake-123456.us-east-2.aws.neon.tech";
  const [dbName, setDbName] = useState("postgres");
  const [dbNameAutoSet, setDbNameAutoSet] = useState(false);
  const [role, setRole] = useState("dreamlit_app");
  const [schemas, setSchemas] = useState("public");
  const [tableCreators, setTableCreators] = useState(TABLE_OWNER_DEFAULT);
  const [tableOwnerAutoSet, setTableOwnerAutoSet] = useState(false);
  const [includeRls, setIncludeRls] = useState(false);
  const [useAllSchemas, setUseAllSchemas] = useState(true);
  const [copyLabel, setCopyLabel] = useState("Copy SQL");
  const [showTooltip, setShowTooltip] = useState(false);
  const [showRlsTooltip, setShowRlsTooltip] = useState(false);
  const [isGenerated, setIsGenerated] = useState(false);
  const [showAdvanced, setShowAdvanced] = useState(false);
  const passwordPlaceholder = "[[REPLACE_WITH_PASSWORD]]";
  const [host, setHost] = useState("");
  const [port, setPort] = useState("5432");
  const [connectionCopyLabel, setConnectionCopyLabel] = useState("Copy Connection String");
  const [hostError, setHostError] = useState("");
  const validateHost = value => {
    if (!value.trim()) return "";
    const trimmed = value.trim();
    if ((/\s/).test(trimmed)) {
      return "Host cannot contain spaces";
    }
    if ((/^https?:\/\//i).test(trimmed)) {
      return "Enter just the hostname without http:// or https://";
    }
    if ((/^(\[[^\]]+\]|[^:]+):\d{1,5}$/).test(trimmed)) {
      return "Move the :port part into the Port field";
    }
    if (trimmed.includes("/")) {
      return "Enter just the hostname without any path";
    }
    const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
    const ipv4Regex = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
    const isLikelyIpv6 = candidate => {
      if (!candidate.includes(":")) return false;
      const raw = candidate.replace(/^\[/, "").replace(/\]$/, "");
      const segments = raw.split(":");
      if (segments.length < 3 || segments.length > 8) return false;
      return segments.every(seg => seg === "" || (/^[0-9a-fA-F]{0,4}$/).test(seg));
    };
    if (ipv4Regex.test(trimmed) || isLikelyIpv6(trimmed)) return "";
    if (!hostnameRegex.test(trimmed)) return "Invalid hostname format";
    const isLocalhost = (/^localhost$/i).test(trimmed);
    const hasDot = trimmed.includes(".");
    if (!hasDot && !isLocalhost) {
      return "Use a full hostname like db.example.com";
    }
    return "";
  };
  const handleHostChange = e => {
    const value = e.target.value;
    setHost(value);
    setHostError(validateHost(value));
  };
  const isHostMissing = !host.trim();
  const canGenerate = !isHostMissing && !hostError;
  const generateHint = isHostMissing ? "Enter a host to generate" : hostError || "";
  const connectionHint = hostError || (!host.trim() ? "Enter a host to generate" : "");
  const inputStyle = {
    padding: "6px 8px",
    border: "1px solid #d1d5db",
    borderRadius: 6,
    fontSize: 13,
    width: "100%",
    boxSizing: "border-box"
  };
  const textStyle = {
    padding: "6px 8px",
    border: "1px solid #d1d5db",
    borderRadius: 6,
    fontSize: 13,
    minHeight: 56,
    width: "100%",
    boxSizing: "border-box",
    resize: "vertical"
  };
  const labelStyle = {
    fontSize: 12,
    fontWeight: 600,
    color: "#374151",
    marginBottom: 4
  };
  const sanitizeList = (value, fallback) => {
    const items = value.split(/[,\n]/).map(v => v.trim()).filter(Boolean);
    return items.length ? items : fallback;
  };
  const quoteIdent = value => `"${value.replace(/"/g, '""')}"`;
  const quoteLiteral = value => `'${value.replace(/'/g, "''")}'`;
  const handleUseAllSchemasChange = checked => {
    setUseAllSchemas(checked);
  };
  const {displaySql, copySql} = useMemo(() => {
    const roleRaw = role.trim() || "dreamlit_app";
    const dbRaw = dbName.trim() || "postgres";
    const schemaList = sanitizeList(schemas, ["public"]);
    const tableCreatorList = sanitizeList(tableCreators, ["postgres"]);
    const parentRole = tableCreatorList[0] || "postgres";
    const targetSchemas = useAllSchemas ? [] : schemaList;
    const sweepComment = useAllSchemas ? "Grant permissions on existing schemas" : "Grant permissions on selected schemas";
    const roleIdent = quoteIdent(roleRaw);
    const roleLiteral = quoteLiteral(roleRaw);
    const parentRoleLiteral = quoteLiteral(parentRole);
    const passwordToken = passwordPlaceholder;
    const buildSql = passwordToken => {
      const statements = [];
      statements.push(`-- Create the dreamlit_app user
DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = ${roleLiteral}) THEN
    CREATE ROLE ${roleIdent} WITH LOGIN PASSWORD ${passwordToken} INHERIT;
  ELSE
    ALTER ROLE ${roleIdent} WITH LOGIN PASSWORD ${passwordToken} INHERIT;
  END IF;
END
$$ LANGUAGE plpgsql;`);
      statements.push(`-- Grant database-level privileges
GRANT CONNECT, CREATE, TEMP ON DATABASE ${quoteIdent(dbRaw)} TO ${roleIdent};
CREATE EXTENSION IF NOT EXISTS pgcrypto;`);
      if (!useAllSchemas) {
        targetSchemas.forEach(schema => {
          const schemaIdent = quoteIdent(schema);
          statements.push(`-- Grants for schema ${schema}
GRANT USAGE ON SCHEMA ${schemaIdent} TO ${roleIdent};
GRANT SELECT, TRIGGER ON ALL TABLES IN SCHEMA ${schemaIdent} TO ${roleIdent};
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${schemaIdent} TO ${roleIdent};
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA ${schemaIdent} TO ${roleIdent};`);
        });
      }
      const sweepWhere = useAllSchemas ? `WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'auth')
      AND nspname NOT LIKE 'pg_toast%'` : `WHERE nspname = ANY (ARRAY[${schemaList.map(quoteLiteral).join(", ")}])
      AND nspname NOT IN ('pg_catalog', 'information_schema', 'auth')
      AND nspname NOT LIKE 'pg_toast%'`;
      statements.push(`-- ${sweepComment} (run as ${parentRole})
DO $$
DECLARE
  target_role text := ${roleLiteral};
  parent_role text := NULLIF(${parentRoleLiteral}, '');
  parent_role_oid oid := CASE
    WHEN parent_role IS NULL THEN NULL
    ELSE (SELECT oid FROM pg_roles WHERE rolname = parent_role)
  END;
  schema_record record;
BEGIN
  IF parent_role IS NOT NULL AND parent_role_oid IS NULL THEN
    RAISE NOTICE 'Role % not found; continuing grants without parent filter', parent_role;
  END IF;

  FOR schema_record IN
    SELECT nspname
    FROM pg_namespace
    ${sweepWhere}
  LOOP
    IF parent_role_oid IS NULL OR has_schema_privilege(parent_role_oid, schema_record.nspname, 'USAGE') THEN
      EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', schema_record.nspname, target_role);
      EXECUTE format('GRANT SELECT, TRIGGER ON ALL TABLES IN SCHEMA %I TO %I', schema_record.nspname, target_role);
      EXECUTE format('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', schema_record.nspname, target_role);
      EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA %I TO %I', schema_record.nspname, target_role);
    END IF;
  END LOOP;
END
$$ LANGUAGE plpgsql;`);
      tableCreatorList.forEach(owner => {
        const ownerIdent = quoteIdent(owner);
        const ownerLiteral = quoteLiteral(owner);
        statements.push(`-- Ensure future schemas/objects created by ${owner} are accessible 
DO $$
DECLARE
  target_role text := ${roleLiteral};
  owner_role text := NULLIF(${ownerLiteral}, '');
  owner_oid oid := CASE
    WHEN owner_role IS NULL THEN NULL
    ELSE (SELECT oid FROM pg_roles WHERE rolname = owner_role)
  END;
BEGIN
  IF owner_role IS NULL OR owner_oid IS NULL THEN
    RAISE NOTICE 'Skipping default privileges: role % not found', owner_role;
  ELSE
    EXECUTE format('ALTER DEFAULT PRIVILEGES FOR ROLE %I GRANT USAGE ON SCHEMAS TO %I', owner_role, target_role);
    EXECUTE format('ALTER DEFAULT PRIVILEGES FOR ROLE %I GRANT SELECT, TRIGGER ON TABLES TO %I', owner_role, target_role);
    EXECUTE format('ALTER DEFAULT PRIVILEGES FOR ROLE %I GRANT USAGE, SELECT ON SEQUENCES TO %I', owner_role, target_role);
    EXECUTE format('ALTER DEFAULT PRIVILEGES FOR ROLE %I GRANT EXECUTE ON FUNCTIONS TO %I', owner_role, target_role);
  END IF;
END
$$ LANGUAGE plpgsql;`);
      });
      if (includeRls) {
        const baseSchemaFilter = `schemaname NOT IN ('pg_catalog', 'information_schema')
    AND schemaname NOT LIKE 'pg_toast%'
    AND schemaname NOT LIKE 'pg_temp_%'`;
        const schemaFilter = useAllSchemas ? baseSchemaFilter : `${baseSchemaFilter} AND schemaname = ANY (ARRAY[${schemaList.map(quoteLiteral).join(", ")}])`;
        statements.push(`-- Add permissive SELECT policies (effective when RLS is enabled)
DO $$
DECLARE
  target_role text := ${roleLiteral};
  parent_role text := NULLIF(${parentRoleLiteral}, '');
  parent_role_oid oid := CASE
    WHEN parent_role IS NULL THEN NULL
    ELSE (SELECT oid FROM pg_roles WHERE rolname = parent_role)
  END;
  policy_base text := regexp_replace(target_role, '[^a-zA-Z0-9]+', '_', 'g');
  policy_reserved_len constant integer := length('dreamlit__select_policy');
  policy_suffix_budget constant integer := 63 - policy_reserved_len;
  policy_suffix text;
  policy_name text;
  table_record record;
  schema_is_accessible boolean;
BEGIN
  -- PostgreSQL identifiers max out at 63 bytes; keep suffix + hash within the remaining budget.
  IF length(policy_base) <= policy_suffix_budget THEN
    policy_suffix := policy_base;
  ELSE
    policy_suffix := substring(policy_base for GREATEST(policy_suffix_budget - 9, 0))
      || '_' || substring(md5(policy_base), 1, 8);
  END IF;
  policy_name := lower(format('dreamlit_%s_select_policy', policy_suffix));

  FOR table_record IN
    SELECT schemaname, tablename
    FROM pg_tables
    WHERE ${schemaFilter} AND rowsecurity = true
  LOOP
    schema_is_accessible := parent_role_oid IS NULL
      OR has_schema_privilege(parent_role_oid, table_record.schemaname, 'USAGE');

    IF schema_is_accessible THEN
      BEGIN
        EXECUTE format(
          'DROP POLICY IF EXISTS %I ON %I.%I',
          policy_name,
          table_record.schemaname,
          table_record.tablename
        );
        EXECUTE format(
          'CREATE POLICY %I ON %I.%I FOR SELECT TO %I USING (true)',
          policy_name,
          table_record.schemaname,
          table_record.tablename,
          target_role
        );
      EXCEPTION
        WHEN others THEN
          RAISE NOTICE 'Skipping Dreamlit RLS policy for %.%: %',
            table_record.schemaname,
            table_record.tablename,
            SQLERRM;
      END;
    END IF;
  END LOOP;
END
$$ LANGUAGE plpgsql;`);
      }
      return statements.join("\n\n").trim();
    };
    const sql = buildSql(passwordToken);
    return {
      displaySql: sql,
      copySql: sql
    };
  }, [dbName, role, schemas, tableCreators, includeRls, useAllSchemas]);
  const handleGenerate = () => {
    if (!canGenerate) return;
    setIsGenerated(true);
  };
  const {displayConnectionString, copyConnectionString, isConnectionReady} = useMemo(() => {
    const roleRaw = role.trim() || "dreamlit_app";
    const passwordRaw = passwordPlaceholder;
    const hostRaw = host.trim();
    const portRaw = port.trim() || "5432";
    const dbRaw = dbName.trim() || "postgres";
    const isReady = Boolean(hostRaw && !validateHost(hostRaw));
    const formatHostForUrl = rawHost => {
      if (!rawHost) return "";
      const needsBrackets = rawHost.includes(":") && !(rawHost.startsWith("[") && rawHost.endsWith("]"));
      return needsBrackets ? `[${rawHost}]` : rawHost;
    };
    const buildDisplayString = () => {
      if (!hostRaw) return "";
      return `postgresql://${roleRaw}:${passwordRaw}@${formatHostForUrl(hostRaw)}:${portRaw}/${dbRaw}`;
    };
    const buildCopyString = () => {
      if (!hostRaw) return "";
      const encodedUser = encodeURIComponent(roleRaw);
      const encodedPassword = "";
      return `postgresql://${encodedUser}:${encodedPassword}@${formatHostForUrl(hostRaw)}:${portRaw}/${dbRaw}`;
    };
    return {
      displayConnectionString: buildDisplayString(),
      copyConnectionString: buildCopyString(),
      isConnectionReady: isReady
    };
  }, [role, host, port, dbName, passwordPlaceholder]);
  const handleCopyConnectionString = async () => {
    try {
      await navigator.clipboard.writeText(copyConnectionString);
      setConnectionCopyLabel("Copied!");
      setTimeout(() => setConnectionCopyLabel("Copy Connection String"), 1800);
    } catch {
      setConnectionCopyLabel("Copy failed");
      setTimeout(() => setConnectionCopyLabel("Copy Connection String"), 1800);
    }
  };
  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(copySql);
      setCopyLabel("Copied!");
      setTimeout(() => setCopyLabel("Copy SQL"), 1800);
    } catch {
      setCopyLabel("Copy failed");
      setTimeout(() => setCopyLabel("Copy SQL"), 1800);
    }
  };
  useEffect(() => {
    setIsGenerated(false);
  }, [dbName, role, host, port, schemas, tableCreators, useAllSchemas, includeRls]);
  const isNeonHost = host.trim().toLowerCase().includes("neon.tech");
  useEffect(() => {
    if (isNeonHost) {
      const owner = tableCreators.trim().toLowerCase();
      if (!tableOwnerAutoSet && (owner === "" || owner === TABLE_OWNER_DEFAULT)) {
        setTableCreators(TABLE_OWNER_NEON);
        setTableOwnerAutoSet(true);
      }
      const loweredDb = dbName.trim().toLowerCase();
      if (!dbNameAutoSet && (loweredDb === "" || loweredDb === "postgres")) {
        setDbName("neondb");
        setDbNameAutoSet(true);
      }
    } else {
      if (tableOwnerAutoSet && tableCreators.trim().toLowerCase() === TABLE_OWNER_NEON) {
        setTableCreators(TABLE_OWNER_DEFAULT);
      }
      if (dbNameAutoSet && dbName.trim().toLowerCase() === "neondb") {
        setDbName("postgres");
      }
      setTableOwnerAutoSet(false);
      setDbNameAutoSet(false);
    }
  }, [isNeonHost, tableCreators, tableOwnerAutoSet, dbName, dbNameAutoSet]);
  const hostPlaceholder = isNeonHost ? HOST_HINT_NEON : HOST_HINT_DEFAULT;
  return <div style={{
    border: "1px solid #e5e7eb",
    borderRadius: 10,
    padding: 14,
    background: "#fafafa",
    margin: "16px 0"
  }}>
      {}
      {}
      <div style={{
    display: "grid",
    gridTemplateColumns: "2fr 120px",
    gap: 8,
    marginBottom: 4
  }}>
        <div>
          <div style={labelStyle}>Host</div>
          <input value={host} onChange={handleHostChange} placeholder={hostPlaceholder} style={{
    ...inputStyle,
    borderColor: hostError ? "#ef4444" : "#d1d5db"
  }} />
          {hostError && <p style={{
    fontSize: 11,
    color: "#ef4444",
    marginTop: 4
  }}>
              {hostError}
            </p>}
        </div>
        <div>
          <div style={labelStyle}>Port</div>
          <input value={port} onChange={e => setPort(e.target.value)} placeholder="5432" style={inputStyle} />
          {}
        </div>
      </div>

      <div style={{
    fontSize: 11,
    color: "#6b7280",
    marginBottom: 16
  }}>
        Use your provider's{" "}
        <span style={{
    fontWeight: 700
  }}>direct database host</span> (not a
        pooler host).
      </div>

      <div style={{
    display: "grid",
    gridTemplateColumns: "1fr 1fr",
    gap: 8,
    marginBottom: 12
  }}>
        <div>
          <div style={labelStyle}>Dreamlit user name</div>
          <input value={role} onChange={e => setRole(e.target.value)} placeholder="dreamlit_app" style={inputStyle} />
        </div>
        <div>
          <div style={labelStyle}>Database name</div>
          <input value={dbName} onChange={e => {
    setDbName(e.target.value);
    setDbNameAutoSet(false);
  }} placeholder="postgres" style={inputStyle} />
        </div>
      </div>

      {}
      <button onClick={() => setShowAdvanced(!showAdvanced)} type="button" style={{
    display: "flex",
    alignItems: "center",
    gap: 8,
    background: "none",
    border: "none",
    padding: 0,
    cursor: "pointer",
    fontSize: 13,
    fontWeight: 600,
    color: "#111827",
    marginBottom: 12
  }}>
        <span style={{
    display: "inline-block",
    transition: "transform 0.2s",
    transform: showAdvanced ? "rotate(90deg)" : "rotate(0deg)"
  }}>
          ▶
        </span>
        Advanced configuration
      </button>

      {showAdvanced && <div style={{
    display: "flex",
    flexDirection: "column",
    gap: 10,
    marginBottom: 14
  }}>
          <div style={{
    display: "grid",
    gridTemplateColumns: "1fr 1fr",
    gap: 10
  }}>
            <div style={{
    display: "flex",
    flexDirection: "column",
    gap: 8,
    padding: "8px 10px",
    background: "#f3f4f6",
    borderRadius: 6
  }}>
              <div style={{
    fontSize: 13,
    fontWeight: 700
  }}>Grant scope</div>
              <label style={{
    display: "flex",
    alignItems: "center",
    gap: 6
  }}>
                <input type="radio" name="scope" checked={useAllSchemas} onChange={() => handleUseAllSchemasChange(true)} style={{
    margin: 0
  }} />
                <span style={{
    fontSize: 13
  }}>
                  All schemas — grants everywhere parent role has USAGE
                </span>
              </label>
              <label style={{
    display: "flex",
    alignItems: "center",
    gap: 6
  }}>
                <input type="radio" name="scope" checked={!useAllSchemas} onChange={() => handleUseAllSchemasChange(false)} style={{
    margin: 0
  }} />
                <span style={{
    fontSize: 13
  }}>
                  Only listed schemas — add them below
                </span>
              </label>
              {!useAllSchemas && <textarea value={schemas} onChange={e => setSchemas(e.target.value)} placeholder="public" style={textStyle} />}
            </div>
            <div style={{
    display: "flex",
    flexDirection: "column",
    gap: 8,
    padding: "8px 10px",
    background: "#f3f4f6",
    borderRadius: 6
  }}>
              <div style={labelStyle}>
                Table owner role(s){" "}
                <span style={{
    position: "relative",
    display: "inline-block"
  }}>
                  <span onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} style={{
    fontWeight: 400,
    color: "#6b7280",
    cursor: "help",
    borderBottom: "1px dotted #9ca3af"
  }}>
                    (for default privileges)
                  </span>
                  {showTooltip && <span style={{
    position: "absolute",
    bottom: "100%",
    left: "50%",
    transform: "translateX(-50%)",
    marginBottom: 6,
    padding: "6px 10px",
    background: "#1f2937",
    color: "#fff",
    fontSize: 11,
    borderRadius: 6,
    whiteSpace: "nowrap",
    zIndex: 40
  }}>
                      The role that creates your tables (usually 'postgres')
                    </span>}
                </span>
              </div>
              <textarea value={tableCreators} onChange={e => {
    setTableCreators(e.target.value);
    setTableOwnerAutoSet(false);
  }} placeholder="postgres" style={textStyle} />
            </div>
          </div>

          <label style={{
    display: "flex",
    alignItems: "center",
    gap: 8,
    padding: "8px 10px",
    background: "#f3f4f6",
    borderRadius: 6,
    cursor: "pointer"
  }}>
            <input type="checkbox" checked={includeRls} onChange={e => setIncludeRls(e.target.checked)} style={{
    margin: 0
  }} />
            <span style={{
    fontSize: 13,
    display: "flex",
    alignItems: "center",
    gap: 8
  }}>
              <span style={{
    fontWeight: 700
  }}>Add RLS policies</span>
              <span onMouseEnter={() => setShowRlsTooltip(true)} onMouseLeave={() => setShowRlsTooltip(false)} style={{
    position: "relative",
    width: 18,
    height: 18,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: "50%",
    background: "#e5e7eb",
    color: "#111827",
    fontSize: 11,
    fontWeight: 700,
    cursor: "help",
    border: "1px solid #d1d5db"
  }} aria-label="More info about RLS policies" title="More info about RLS policies">
                i
                {showRlsTooltip && <span style={{
    position: "absolute",
    bottom: "125%",
    left: "50%",
    transform: "translateX(-50%)",
    marginBottom: 6,
    padding: "8px 10px",
    background: "#111827",
    color: "#f9fafb",
    fontSize: 11,
    borderRadius: 6,
    whiteSpace: "normal",
    width: 260,
    boxShadow: "0 8px 16px rgba(0,0,0,0.25)",
    zIndex: 40,
    lineHeight: 1.4
  }}>
                    Adds a permissive SELECT policy on every RLS-enabled
                    application table in the chosen schemas. Dreamlit handles
                    its own internal-table RLS setup automatically when it
                    creates the `dreamlit` schema.
                  </span>}
              </span>
              <span style={{
    color: "#6b7280"
  }}>
                — Creates application-table SELECT policies when RLS is enabled
              </span>
            </span>
          </label>
        </div>}

      {}
      <div style={{
    marginBottom: 12,
    display: "flex",
    gap: 10,
    alignItems: "center",
    flexWrap: "wrap"
  }}>
        <button onClick={handleGenerate} disabled={!canGenerate} style={{
    padding: "8px 16px",
    borderRadius: 6,
    border: "none",
    background: "#111827",
    color: "white",
    fontSize: 13,
    fontWeight: 600,
    cursor: !canGenerate ? "not-allowed" : "pointer",
    opacity: !canGenerate ? 0.55 : 1
  }} type="button">
          Generate commands and connection string
        </button>
        {generateHint && <span style={{
    fontSize: 12,
    color: "#6b7280"
  }}>{generateHint}</span>}
        <span style={{
    fontSize: 12,
    color: "#4b5563"
  }}>
          Generated locally in your browser. Nothing is sent to Dreamlit or
          stored.
        </span>
      </div>

      {}
      {isGenerated && <div style={{
    marginTop: 8
  }}>
          <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 8,
    marginBottom: 8
  }}>
            <h3 style={{
    margin: 0,
    fontSize: 15,
    color: "#111827"
  }}>
              SQL commands to run
            </h3>
            <button onClick={handleCopy} style={{
    padding: "8px 12px",
    borderRadius: 6,
    border: "none",
    background: "#111827",
    color: "white",
    fontSize: 12,
    cursor: "pointer"
  }} type="button">
              {copyLabel}
            </button>
          </div>
          <div style={{
    border: "1px solid #e5e7eb",
    borderRadius: 12,
    padding: 0,
    background: "#f9fafb",
    color: "#111827",
    minHeight: 200
  }}>
            <pre style={{
    margin: 0,
    whiteSpace: "pre-wrap",
    overflowX: "auto",
    maxHeight: 340,
    overflowY: "auto",
    fontSize: 12,
    lineHeight: 1.55,
    wordBreak: "break-word"
  }}>
              <code>{displaySql}</code>
            </pre>
          </div>
        </div>}

      {}
      {isGenerated && <div style={{
    marginTop: 16
  }}>
          <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 8,
    marginBottom: 8
  }}>
            <h3 style={{
    margin: 0,
    fontSize: 15,
    color: "#111827"
  }}>
              Your connection string
            </h3>
            <button onClick={handleCopyConnectionString} disabled={!isConnectionReady} style={{
    padding: "8px 12px",
    borderRadius: 6,
    border: "none",
    background: "#111827",
    color: "white",
    fontSize: 12,
    cursor: isConnectionReady ? "pointer" : "not-allowed",
    opacity: isConnectionReady ? 1 : 0.55
  }} type="button">
              {connectionCopyLabel}
            </button>
          </div>
          <div style={{
    border: "1px solid #e5e7eb",
    borderRadius: 12,
    padding: 0,
    background: "#f9fafb",
    color: "#111827"
  }}>
            <pre style={{
    margin: 0,
    whiteSpace: "pre-wrap",
    overflowX: "auto",
    fontSize: 12,
    lineHeight: 1.55,
    wordBreak: "break-word",
    color: displayConnectionString ? undefined : "#6b7280",
    userSelect: "none"
  }}>
              <code>
                {displayConnectionString || "postgresql://dreamlit_app:<PASSWORD>@<HOST>:<PORT>/<DATABASE>"}
              </code>
            </pre>
          </div>
          {!isConnectionReady && <p style={{
    fontSize: 11,
    color: "#6b7280",
    marginTop: 6
  }}>
              {connectionHint}
            </p>}
        </div>}
    </div>;
};

<OpenAccordionsOnHash />

<Tip>
  **Using cloud-hosted Supabase?** This guide is for generic PostgreSQL connections (including self-hosted Supabase). If you're on Supabase's cloud platform, use the [Supabase guide](/docs/configuration/data-sources/supabase) instead for a one-click OAuth2 setup.
</Tip>

Dreamlit integrates with any PostgreSQL-compatible database (including self-hosted Supabase,
Neon, RDS, Cloud SQL, Render, Railway, etc.), letting you trigger
notification workflows from database events.

## How Dreamlit interacts with your database

On a high level, Dreamlit needs to know where your data lives in your database, install database triggers (for database trigger workflows), and manage some housekeeping tables in a separate schema.

This way:

* Dreamlit's AI can propose the relevant workflows for you by introspecting your database structure to understand the shape of your data and where it lives.
* You can preview your workflows with live database rows.
* You can react to new events happening in your app in real-time.

Dreamlit **never** edits, deletes, inserts, or otherwise modifies data in your database.

For a full technical deep dive, check out the [how it works](/docs/getting-started/how-it-works) page.

<Note>
  Dreamlit takes security seriously. To review all the measures we take, see [Security](/docs/resources/security).
</Note>

### Permissions needed

The Postgres user/role you provide to Dreamlit must at least have the following permissions:

* Read relevant tables (`SELECT`) so Dreamlit's AI can set up workflows for you and you can preview workflows with live database rows.
* Create triggers on tables to launch workflows right away when events occur.
* Manage its own `dreamlit` schema for housekeeping and logging.

Dreamlit **does not** need edit, delete, or insert permissions on your tables as it never mutates or modifies your data.

Of course, if you already have a user with sufficient privileges (such as a superuser) you can use that.

**However, it is best practice to create a separate Postgres user for each service that needs access to your database, following the principles
of isolation and least privilege.** Instructions to do this are below.

## Commands to run to set up your `dreamlit_app` database user

<Tip>
  It's recommended to create a separate Postgres database user for each service that needs access to your database.

  This way, you can grant only the necessary permissions for each service and you can selectively revoke access as needed.
</Tip>

Dreamlit needs the permissions as outlined in the previous section. Follow the instructions below to set up the `dreamlit_app` user and grant the necessary permissions.

1. Generate a strong, unique password on your own (e.g., with a [password generator](https://1password.com/password-generator)).
2. Fill in the details for your database connection in the applet below. For the host, use your provider's **direct database host** (typically port `5432`), not a transaction pooler host.
3. Hit **Generate**.
4. Copy the SQL commands and run them in your database as a superuser (e.g., `postgres` or `neondb_owner`), substituting `[[REPLACE_WITH_PASSWORD]]` with your password from step 1 with single quotes like `'mypassword'`. This creates the `dreamlit_app` user and grants the minimal necessary permissions.
5. Copy the connection string (after replacing the password placeholder, this time without single quotes) into Dreamlit to connect your database and finish setup.

<PostgresSqlBuilder />

<Warning>
  **If you're using RLS (row level security)**, be sure to turn on the **Add RLS policies** option in Advanced configuration in the applet above so that the `dreamlit_app` can `SELECT` rows on RLS-enabled application tables.

  For any application tables you create in the future, you will need to ensure there is an appropriate RLS policy created for the `dreamlit_app` to `SELECT` rows, by either adding policies manually or rerunning the generator above (the commands generated are idempotent, so you can rerun them as needed).
</Warning>

<Accordion title="Don't want to use the applet? You can build the commands yourself instead">
  Run these SQL commands in your database as a superuser (e.g., `postgres`), or ideally as the same role that creates schemas/tables in your app so default privileges attach to future schemas:

  ### 1. Create a dedicated Dreamlit user

  First, we create a new database user specifically for Dreamlit with a secure password. Be sure to replace `<SECRET_PASSWORD>` below with a
  [strong, unique password](https://1password.com/password-generator).

  ```sql theme={null}
  CREATE USER dreamlit_app WITH PASSWORD '<SECRET_PASSWORD>' INHERIT;
  ```

  ### 2. Grant database-level privileges

  Grant the Dreamlit user basic database access. Be sure to replace `<YOUR_DATABASE_NAME>` below
  with the name of your database (e.g., `defaultdb` or `postgres`).

  ```sql theme={null}
  GRANT CONNECT ON DATABASE <YOUR_DATABASE_NAME> TO dreamlit_app;
  GRANT CREATE ON DATABASE <YOUR_DATABASE_NAME> TO dreamlit_app;
  GRANT TEMP ON DATABASE <YOUR_DATABASE_NAME> TO dreamlit_app;
  ```

  ### 3. Install pgcrypto (required)

  `gen_random_uuid()` is used by Dreamlit's housekeeping objects. Install the extension in the target database:

  ```sql theme={null}
  CREATE EXTENSION IF NOT EXISTS pgcrypto;
  ```

  <div id="4-grant-view--trigger-permissions-on-your-application-tables" />

  ### 4. Grant view + trigger permissions on your application tables

  Grant the Dreamlit user access to your schemas and set default privileges so future tables work too. Replace `<YOUR_SCHEMA_NAME>` with each schema (e.g., `public`) and `<TABLE_CREATOR_ROLE>` with the role that creates future tables (your migrations/app owner). Two knobs:

  * **Scope**: run the GRANT block for each schema you want to expose. If you prefer “all schemas”, run the backfill block below once.
  * **Future objects**: global default privileges (shown here) apply to all future schemas/objects created by `<TABLE_CREATOR_ROLE>`. If you prefer schema-scoped defaults, swap the global lines for `... IN SCHEMA <YOUR_SCHEMA_NAME> ...`.

  ```sql theme={null}
  GRANT USAGE ON SCHEMA <YOUR_SCHEMA_NAME> TO dreamlit_app;
  GRANT SELECT, TRIGGER ON ALL TABLES IN SCHEMA <YOUR_SCHEMA_NAME> TO dreamlit_app;
  GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA <YOUR_SCHEMA_NAME> TO dreamlit_app;
  GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA <YOUR_SCHEMA_NAME> TO dreamlit_app;

  -- Global defaults so future schemas/objects created by this role are accessible
  ALTER DEFAULT PRIVILEGES FOR ROLE <TABLE_CREATOR_ROLE>
    GRANT USAGE ON SCHEMAS TO dreamlit_app;
  ALTER DEFAULT PRIVILEGES FOR ROLE <TABLE_CREATOR_ROLE>
    GRANT SELECT, TRIGGER ON TABLES TO dreamlit_app;
  ALTER DEFAULT PRIVILEGES FOR ROLE <TABLE_CREATOR_ROLE>
    GRANT USAGE, SELECT ON SEQUENCES TO dreamlit_app;
  ALTER DEFAULT PRIVILEGES FOR ROLE <TABLE_CREATOR_ROLE>
    GRANT EXECUTE ON FUNCTIONS TO dreamlit_app;
  ```

  **Optional: Sync existing schemas**
  Run this once as the table owner (e.g., `postgres`) to backfill grants across all existing schemas (grants on current objects; use this if you chose “all schemas” or want to catch up existing ones). Global defaults above already cover future objects; remove the global lines if you prefer per-schema defaults and swap in `IN SCHEMA ...` per your needs.

  ```sql theme={null}
  DO $$
  DECLARE
    target_role text := 'dreamlit_app';
    parent_role text := '<TABLE_CREATOR_ROLE>';
    schema_record record;
  BEGIN
    FOR schema_record IN
      SELECT nspname
      FROM pg_namespace
      WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'auth')
        AND nspname NOT LIKE 'pg_toast%'
    LOOP
      IF parent_role IS NULL OR has_schema_privilege(parent_role, schema_record.nspname, 'USAGE') THEN
        EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', schema_record.nspname, target_role);
        EXECUTE format('GRANT SELECT, TRIGGER ON ALL TABLES IN SCHEMA %I TO %I', schema_record.nspname, target_role);
        EXECUTE format('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', schema_record.nspname, target_role);
        EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA %I TO %I', schema_record.nspname, target_role);
      END IF;
    END LOOP;
  END;
  $$;
  ```

  <Accordion title="How to find the table owner role" icon="circle-question">
    Run this query to see which role owns your existing tables:

    ```sql theme={null}
    SELECT tableowner, COUNT(*) as table_count
    FROM pg_tables
    WHERE schemaname = '<YOUR_SCHEMA_NAME>'
    GROUP BY tableowner;
    ```

    Use the role name from the `tableowner` column in the `ALTER DEFAULT PRIVILEGES` commands above.
  </Accordion>

  If any of your application tables have RLS enabled (common for self-hosted Supabase projects if you use the "anon" role), you must create
  policies that allow the newly created `dreamlit_app` to `SELECT` rows. Otherwise,
  you won't be able to read the data needed for your Dreamlit workflows.

  If you're not sure, you can run the following command to check which tables have RLS enabled:

  ```sql theme={null}
  -- First, check which tables have RLS enabled
  SELECT schemaname, tablename, rowsecurity
  FROM pg_tables
  WHERE schemaname = '<YOUR_SCHEMA_NAME>' AND rowsecurity = true;
  ```

  If the above query returns any rows, then you have RLS enabled and
  you'll need to create a policy for each table that allows the `dreamlit_app` to `SELECT` rows.

  Run the following command to generate the policy creation commands for all RLS-enabled tables:

  ```sql theme={null}
  -- Generate CREATE POLICY commands for all RLS-enabled tables
  SELECT 'CREATE POLICY dreamlit_dreamlit_app_select_policy ON ' || schemaname || '.' || tablename ||
        ' FOR SELECT TO dreamlit_app USING (true);' as policy_command
  FROM pg_tables
  WHERE schemaname = '<YOUR_SCHEMA_NAME>' AND rowsecurity = true;
  ```

  Finally, copy and execute the commands returned from the above query.

  **For future tables:**
  Unfortunately, PostgreSQL doesn't support default RLS policies like it does for privileges. You'll need to remember to create a policy
  whenever you add a new table with RLS enabled.

  Dreamlit provisions its own internal `dreamlit` schema separately during connection setup, so you only need to think about RLS for your application tables here.

  ### 5. Construct your new connection string

  Construct your PostgreSQL connection string using the `dreamlit_app` and the `<SECRET_PASSWORD>` you created. Provide this string to Dreamlit.

  <AccordionGroup>
    <Accordion title="Self-hosted Supabase">
      Use the direct connection details from your self-hosted Supabase instance.

      ```
      postgresql://dreamlit_app:<SECRET_PASSWORD>@<YOUR_SUPABASE_HOST>:5432/postgres
      ```

      * Replace `<YOUR_SUPABASE_HOST>` with your self-hosted Supabase database host.
    </Accordion>

    <Accordion title="Neon">
      Use the **Direct connection** details from your Neon dashboard. The username is `dreamlit_app`.

      ```
      postgresql://dreamlit_app:<SECRET_PASSWORD>@<NEON_DIRECT_HOST>:5432/<NEON_DB_NAME>
      ```

      * Replace `<NEON_DIRECT_HOST>` and `<NEON_DB_NAME>` with values from your Neon Connection Details.
    </Accordion>

    <Accordion title="Standard Databases (AWS RDS, Google Cloud, Azure, Heroku, DigitalOcean, Direct Connection)">
      Use your standard direct database connection details.

      ```
      postgresql://dreamlit_app:<SECRET_PASSWORD>@<YOUR_DATABASE_HOST>:<PORT>/<YOUR_DATABASE_NAME>
      ```

      * Replace `<YOUR_DATABASE_HOST>`, `<PORT>` (usually 5432), and `<YOUR_DATABASE_NAME>` with your database's connection details.
    </Accordion>
  </AccordionGroup>

  Copy the correctly formatted connection string and paste it into the Dreamlit connection setup field.
</Accordion>

### Troubleshooting

<AccordionGroup>
  <Accordion title="Troubleshooting: 'must be owner of relation X'">
    If you see an error like `permission denied for table X` or `must be owner of relation X` when Dreamlit tries to create triggers, it means the Dreamlit user lacks TRIGGER permission (or ownership) on that table.

    Quick checks:

    * Confirm Postgres version is 14+ (TRIGGER privilege is grantable starting in 14).
    * Verify the grant actually landed: `SELECT has_table_privilege('dreamlit_app','<SCHEMA>.<TABLE>','TRIGGER');`

    **Solution (pick one):**

    ```sql theme={null}
    -- Grant trigger rights on the table
    GRANT TRIGGER ON TABLE <SCHEMA>.<TABLE> TO dreamlit_app;

    -- For future tables in the schema (run as the table owner, often postgres or your app role)
    ALTER DEFAULT PRIVILEGES FOR ROLE <TABLE_CREATOR_ROLE> IN SCHEMA <SCHEMA>
      GRANT TRIGGER ON TABLES TO dreamlit_app;

    -- Or transfer ownership to a dedicated owner role you control:
    ALTER TABLE <SCHEMA>.<TABLE> OWNER TO <APP_OWNER_ROLE>;
    GRANT <APP_OWNER_ROLE> TO dreamlit_app;
    ```
  </Accordion>

  <Accordion title="Troubleshooting: Permission denied for schema dreamlit">
    If you see `permission denied for schema dreamlit`, the schema privileges were not applied correctly.

    **Fix (run as the schema owner or a superuser):**

    1. Find who owns the schema:

    ```sql theme={null}
    SELECT schema_owner
    FROM information_schema.schemata
    WHERE schema_name = 'dreamlit';
    ```

    2. Easiest path: make `dreamlit_app` the owner so it can self-manage:

    ```sql theme={null}
    ALTER SCHEMA dreamlit OWNER TO dreamlit_app;
    ```

    3. If you must keep the current owner, grant explicit rights to `dreamlit_app`:

    ```sql theme={null}
    GRANT USAGE, CREATE ON SCHEMA dreamlit TO dreamlit_app;
    GRANT ALL ON ALL TABLES IN SCHEMA dreamlit TO dreamlit_app;
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA dreamlit TO dreamlit_app;
    GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA dreamlit TO dreamlit_app;
    ```

    4. Keep future objects accessible (replace `<SCHEMA_OWNER>` with the owner from step 1):

    ```sql theme={null}
    ALTER DEFAULT PRIVILEGES FOR ROLE <SCHEMA_OWNER> IN SCHEMA dreamlit
      GRANT ALL ON TABLES TO dreamlit_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE <SCHEMA_OWNER> IN SCHEMA dreamlit
      GRANT USAGE, SELECT ON SEQUENCES TO dreamlit_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE <SCHEMA_OWNER> IN SCHEMA dreamlit
      GRANT EXECUTE ON FUNCTIONS TO dreamlit_app;
    ```
  </Accordion>

  <Accordion title="Troubleshooting: relation &#x22;event_log&#x22; already exists">
    This appears when Dreamlit tries to create its internal table but a prior install left `dreamlit.event_log` owned by another role (or with different grants).

    **Fix (pick one, run as schema owner/superuser):**

    1. Reuse the existing table by granting Dreamlit access:

    ```sql theme={null}
    GRANT INSERT, SELECT, DELETE ON TABLE dreamlit.event_log TO dreamlit_app;
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA dreamlit TO dreamlit_app;
    ```

    2. If you prefer Dreamlit to own the table (and the data inside is only Dreamlit logs), transfer ownership:

    ```sql theme={null}
    ALTER TABLE dreamlit.event_log OWNER TO dreamlit_app;
    ```

    3. If it’s safe to rebuild the log (non-production or you don’t need the old rows), drop and let Dreamlit recreate:

    ```sql theme={null}
    DROP TABLE dreamlit.event_log;
    ```

    After any option, rerun Test Connection. If you keep a non-Dreamlit owner, also set default privileges so new Dreamlit objects stay accessible:

    ```sql theme={null}
    ALTER DEFAULT PRIVILEGES FOR ROLE <SCHEMA_OWNER> IN SCHEMA dreamlit
      GRANT ALL ON TABLES TO dreamlit_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE <SCHEMA_OWNER> IN SCHEMA dreamlit
      GRANT USAGE, SELECT ON SEQUENCES TO dreamlit_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE <SCHEMA_OWNER> IN SCHEMA dreamlit
      GRANT EXECUTE ON FUNCTIONS TO dreamlit_app;
    ```
  </Accordion>

  <Accordion title="Troubleshooting: Dreamlit can't see any rows">
    If Dreamlit reports zero rows in all tables, it might be because you have RLS enabled and you haven't added a `SELECT` policy for `dreamlit_app`.

    **Fix:** add the right RLS policy for the application tables Dreamlit needs to read.

    * For application tables, rerun the generator above with **Add RLS policies** enabled in Advanced configuration (or add `SELECT` policies manually) so `dreamlit_app` can read data.
    * For new RLS application tables you create later, add a `SELECT` policy for `dreamlit_app` or rerun the generator.
  </Accordion>
</AccordionGroup>

### Uninstalling Dreamlit

If you ever want to remove Dreamlit entirely:

1. **Unpublish** all workflows that depend on your database connection (this removes triggers/functions).
2. **Delete** the connection in the Dreamlit dashboard.
3. (Optional) **Drop** the `dreamlit` schema and the `dreamlit_app` database user to remove all Dreamlit-managed objects.

<AccordionGroup>
  <Accordion title="Commands to remove Dreamlit user">
    If you'd like to drop the `dreamlit_app` role, you can issue the following commands. Replace `<YOUR_DATABASE_NAME>`, `<YOUR_SCHEMA_NAME>`, and `<TABLE_CREATOR_ROLE>` as appropriate. For **credential rotation only**, skip the optional DROP steps and just update the role password instead.

    ```sql theme={null}
    -- Revoke privileges on your schema (e.g., public)
    -- Replace <TABLE_CREATOR_ROLE> with the same role used when granting default privileges
    REVOKE SELECT, TRIGGER ON ALL TABLES IN SCHEMA <YOUR_SCHEMA_NAME> FROM dreamlit_app;
    REVOKE USAGE, SELECT ON ALL SEQUENCES IN SCHEMA <YOUR_SCHEMA_NAME> FROM dreamlit_app;
    REVOKE USAGE ON SCHEMA <YOUR_SCHEMA_NAME> FROM dreamlit_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE <TABLE_CREATOR_ROLE> IN SCHEMA <YOUR_SCHEMA_NAME>
      REVOKE SELECT, TRIGGER ON TABLES FROM dreamlit_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE <TABLE_CREATOR_ROLE> IN SCHEMA <YOUR_SCHEMA_NAME>
      REVOKE USAGE, SELECT ON SEQUENCES FROM dreamlit_app;

    -- Optional: uninstall Dreamlit objects (this drops Dreamlit logs)
    REASSIGN OWNED BY dreamlit_app TO <TABLE_CREATOR_ROLE>;
    DROP SCHEMA IF EXISTS dreamlit CASCADE;

    -- Clean up any remaining objects owned by the role (outside your schema)
    DROP OWNED BY dreamlit_app;

    -- Revoke database privileges
    REVOKE CONNECT, CREATE, TEMP ON DATABASE <YOUR_DATABASE_NAME> FROM dreamlit_app;

    -- Finally, drop the user
    DROP USER dreamlit_app;
    ```
  </Accordion>
</AccordionGroup>

<Check>
  That's it! By connecting Postgres to Dreamlit, you can create powerful,
  real-time workflows triggered by database changes — without writing any custom
  code in your application.
</Check>
