Grafo de Cadeia Societária com Neo4j

Construa uma aplicação web interativa que visualiza cadeias de participação societária como um grafo.

Introduction

30 Second Summary

Every company has owners. Sometimes those owners are other companies, which are owned by more companies, forming chains that can run several levels deep. Understanding who ultimately controls an entity is critical for compliance, investment due diligence, and fraud detection.

In this project, you will build an interactive web application that visualizes corporate ownership chains as a graph. You will model companies and people as nodes, connect them with ownership relationships, and explore the structure through queries and a live visualization.

What You'll Build

A web-based ownership graph visualizer powered by Neo4j and neovis.js that renders companies and people as color-coded, draggable nodes connected by ownership percentages.

By the end of this project, you'll have:

  • A Neo4j AuraDB Free instance populated with a realistic multi-level corporate ownership structure.
  • Working Cypher queries that traverse ownership chains, measure depth, and identify ultimate beneficial owners.
  • An interactive graph visualization with draggable nodes, zoom, hover details, and query buttons that update the view dynamically.
  • Secret Mission: a click-to-trace feature that highlights the ownership path from any selected entity up to its ultimate beneficial owner.

Are there any prerequisites?

You should have basic familiarity with SQL concepts (tables, queries) and basic Python knowledge (running scripts, installing packages). Node.js should already be installed on your machine. No prior experience with graph databases is needed.

Set Up Neo4j and the Project

Before you can model ownership data as a graph, you need a graph database to store it. Neo4j AuraDB Free gives you a fully managed cloud instance at no cost, so there is nothing to install locally.

In this step, you will create your Neo4j AuraDB Free instance, save its connection credentials, and set up a local project folder with the Python driver installed.

In this step, get ready to:
  • Create a Neo4j AuraDB Free instance and save its credentials.
  • Install the Neo4j Python driver in a virtual environment.
  • Create the project files for the population script and visualization page.
Create a Neo4j AuraDB Free instance

AuraDB Free is Neo4j's cloud-hosted graph database tier. It requires no credit card and stays free forever.

  • Open your browser and navigate to console.neo4j.io.
  • Create an account using your email or a social login (Google, GitHub, etc.).
  • Once logged in, click Instances in the left sidebar.
  • Click Create Instance.
  • Select the AuraDB Free tier from the options presented.

What is AuraDB Free?

AuraDB Free is a fully managed Neo4j graph database hosted on Google Cloud (us-central1 region). You get up to 200,000 nodes and 400,000 relationships at no cost.

For this project you will create about 15 nodes and 20 relationships, so the free tier is more than enough.

A modal will appear with your instance credentials. This is the only time the password is shown.

  • Click Download and continue to save the credentials file to your computer.
  • Open the downloaded file and confirm it contains three values: NEO4J_URI, NEO4J_USERNAME, and NEO4J_PASSWORD.

Save your password now

Neo4j does not store your password. If you lose it, you will need to reset it from the console. Keep the credentials file somewhere safe on your computer.

  • Wait for the instance status to change from Creating to Running in the Aura console. This usually takes 1-2 minutes.

What does the connection URI look like?

Your NEO4J_URI will follow the format neo4j+s://your-instance-id.databases.neo4j.io. The neo4j+s:// scheme means the connection uses encrypted Bolt over WebSocket.

The default username is always neo4j.

Install the Python Neo4j driver

The Neo4j Python driver lets your script connect to AuraDB and run Cypher queries. You will install it inside a virtual environment to keep your system Python clean.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir cadeia-societaria
cd cadeia-societaria
  • Create a Python virtual environment and activate it by running:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

What is a virtual environment?

A virtual environment is an isolated Python space where packages are installed without affecting your system Python. You will see (venv) at the start of your terminal prompt once it is active.

  • Install the Neo4j Python driver by running:
pip install neo4j==5.28.4

Why pin the version?

Pinning to 5.28.4 ensures everyone following this project gets the same behavior. This version supports Python 3.7 and above, so it works with any recent Python install.

  • Verify the installation succeeded by running:
python -c "import neo4j; print(neo4j.__version__)"

You should see 5.28.4 printed in your terminal.

Seeing ModuleNotFoundError?

Make sure your virtual environment is active. Look for (venv) at the beginning of your prompt. If it is missing, run the activate command again from your cadeia-societaria folder.

Help me troubleshoot my virtual environment

Create the project files

Your project needs two files: a Python script to populate the graph and an HTML page for the visualization.

  • Create both empty files by running:

macOS

touch populate_graph.py index.html

Windows

New-Item populate_graph.py, index.html
  • Confirm the files exist by running:
ls

You should see populate_graph.py and index.html listed in the output (along with the venv folder).

Files not showing up?

Make sure you are still inside the cadeia-societaria folder. Run pwd (macOS) or cd (Windows) to check your current directory.

Help me find my project files

Your environment is ready. Next up, you will design the ownership graph model and populate your AuraDB instance with a realistic corporate structure.

Model and Populate the Ownership Graph

Your Neo4j AuraDB instance is running and your Python environment is ready.

Now you need to fill that empty database with data that represents a real corporate ownership structure. In this step, you will design a graph model with companies, people, and ownership relationships, then populate it using a Python script.

In this step, get ready to:
  • Design the graph data model with Empresa, Pessoa, and PARTICIPA_DE.
  • Write and run a Python script that populates the ownership structure.
  • Verify the graph visually in Neo4j Query.
Design the graph model

In a graph database, data is stored as nodes (entities) and relationships (connections between them). Each node and relationship can hold properties with additional details.

For your corporate ownership structure, the model uses two types of nodes and one relationship type:

  • Empresa nodes represent companies. Each has nome, cnpj, and tipo properties.
  • Pessoa nodes represent individuals. Each has nome and cpf properties.
  • PARTICIPA_DE relationships connect owners to the entities they own. Each carries percentual (ownership percentage) and data_entrada (date of entry) properties.

Why a graph database instead of SQL?

Ownership chains are inherently relational. In SQL, tracing multi-level ownership requires recursive CTEs that become complex and slow as depth increases.

Neo4j traverses relationships in constant time regardless of total data size. A 7-hop ownership traversal that takes 12 seconds in SQL completes in about 13 milliseconds in Neo4j.

The script you are about to write creates a 3-level ownership structure: individual shareholders at the top, a holding company in the middle, and subsidiaries branching downward with cross-ownership for added complexity.

  • Open populate_graph.py in your editor and paste the following connection setup:
from neo4j import GraphDatabase
import os

NEO4J_URI = "neo4j+s://your-instance-id.databases.neo4j.io"
NEO4J_USERNAME = "neo4j"
NEO4J_PASSWORD = "your-password-here"

driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))


def clear_database(tx):
    tx.run("MATCH (n) DETACH DELETE n")

What does this code do?

The first few lines import the Neo4j driver and set your connection credentials. Replace the values of NEO4J_URI and NEO4J_PASSWORD with the values from the credentials file you downloaded earlier.

The clear_database function uses the Cypher query MATCH (n) DETACH DELETE n to wipe all existing data. This ensures a clean slate every time you run the script.

  • Replace your-instance-id.databases.neo4j.io with your actual Neo4j URI from the credentials file.
  • Replace your-password-here with the password from your credentials file.

Keep your credentials file handy

The password was shown only once during instance creation. If you saved the credentials file, open it now and copy the values directly. If you lost the password, you can reset it from the Aura console.

Next, add the function that creates all the nodes and relationships. This is the core of your ownership model.

  • Below the clear_database function, paste the create_ownership_structure function:
def create_ownership_structure(tx):
    # Create Pessoa nodes (individuals)
    tx.run("""
        CREATE (p1:Pessoa {nome: 'Carlos Silva', cpf: '111.222.333-44'})
        CREATE (p2:Pessoa {nome: 'Maria Santos', cpf: '555.666.777-88'})
        CREATE (p3:Pessoa {nome: 'Joao Oliveira', cpf: '999.000.111-22'})
    """)

    # Create Empresa nodes (companies)
    tx.run("""
        CREATE (e1:Empresa {nome: 'Holding Alpha', cnpj: '11.111.111/0001-01', tipo: 'Holding'})
        CREATE (e2:Empresa {nome: 'Tech Beta', cnpj: '22.222.222/0001-02', tipo: 'Tecnologia'})
        CREATE (e3:Empresa {nome: 'Comercio Gamma', cnpj: '33.333.333/0001-03', tipo: 'Comercio'})
        CREATE (e4:Empresa {nome: 'Servicos Delta', cnpj: '44.444.444/0001-04', tipo: 'Servicos'})
        CREATE (e5:Empresa {nome: 'Logistica Epsilon', cnpj: '55.555.555/0001-05', tipo: 'Logistica'})
        CREATE (e6:Empresa {nome: 'Financeira Zeta', cnpj: '66.666.666/0001-06', tipo: 'Financeira'})
        CREATE (e7:Empresa {nome: 'Construtora Eta', cnpj: '77.777.777/0001-07', tipo: 'Construcao'})
        CREATE (e8:Empresa {nome: 'Midia Theta', cnpj: '88.888.888/0001-08', tipo: 'Midia'})
        CREATE (e9:Empresa {nome: 'Agro Iota', cnpj: '99.999.999/0001-09', tipo: 'Agronegocio'})
        CREATE (e10:Empresa {nome: 'Energia Kappa', cnpj: '10.101.010/0001-10', tipo: 'Energia'})
        CREATE (e11:Empresa {nome: 'Saude Lambda', cnpj: '12.121.212/0001-11', tipo: 'Saude'})
        CREATE (e12:Empresa {nome: 'Educacao Mu', cnpj: '13.131.313/0001-12', tipo: 'Educacao'})
    """)

    # Create ownership relationships (Pessoa -> Empresa)
    tx.run("""
        MATCH (p1:Pessoa {nome: 'Carlos Silva'})
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        CREATE (p1)-[:PARTICIPA_DE {percentual: 60.0, data_entrada: '2015-03-01'}]->(e1)
    """)

    tx.run("""
        MATCH (p2:Pessoa {nome: 'Maria Santos'})
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        CREATE (p2)-[:PARTICIPA_DE {percentual: 40.0, data_entrada: '2015-03-01'}]->(e1)
    """)

    tx.run("""
        MATCH (p3:Pessoa {nome: 'Joao Oliveira'})
        MATCH (e3:Empresa {nome: 'Comercio Gamma'})
        CREATE (p3)-[:PARTICIPA_DE {percentual: 25.0, data_entrada: '2020-06-15'}]->(e3)
    """)

    # Create ownership relationships (Empresa -> Empresa) - Level 1
    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e2:Empresa {nome: 'Tech Beta'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 80.0, data_entrada: '2016-01-10'}]->(e2)
    """)

    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e3:Empresa {nome: 'Comercio Gamma'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 75.0, data_entrada: '2016-05-20'}]->(e3)
    """)

    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e4:Empresa {nome: 'Servicos Delta'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 100.0, data_entrada: '2017-02-01'}]->(e4)
    """)

    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e6:Empresa {nome: 'Financeira Zeta'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 55.0, data_entrada: '2018-08-12'}]->(e6)
    """)

    # Create ownership relationships (Empresa -> Empresa) - Level 2
    tx.run("""
        MATCH (e2:Empresa {nome: 'Tech Beta'})
        MATCH (e5:Empresa {nome: 'Logistica Epsilon'})
        CREATE (e2)-[:PARTICIPA_DE {percentual: 51.0, data_entrada: '2019-03-15'}]->(e5)
    """)

    tx.run("""
        MATCH (e3:Empresa {nome: 'Comercio Gamma'})
        MATCH (e7:Empresa {nome: 'Construtora Eta'})
        CREATE (e3)-[:PARTICIPA_DE {percentual: 60.0, data_entrada: '2019-07-01'}]->(e7)
    """)

    tx.run("""
        MATCH (e4:Empresa {nome: 'Servicos Delta'})
        MATCH (e8:Empresa {nome: 'Midia Theta'})
        CREATE (e4)-[:PARTICIPA_DE {percentual: 70.0, data_entrada: '2020-01-10'}]->(e8)
    """)

    tx.run("""
        MATCH (e6:Empresa {nome: 'Financeira Zeta'})
        MATCH (e9:Empresa {nome: 'Agro Iota'})
        CREATE (e6)-[:PARTICIPA_DE {percentual: 45.0, data_entrada: '2020-11-05'}]->(e9)
    """)

    # Create ownership relationships (Empresa -> Empresa) - Level 3
    tx.run("""
        MATCH (e5:Empresa {nome: 'Logistica Epsilon'})
        MATCH (e10:Empresa {nome: 'Energia Kappa'})
        CREATE (e5)-[:PARTICIPA_DE {percentual: 90.0, data_entrada: '2021-04-20'}]->(e10)
    """)

    tx.run("""
        MATCH (e7:Empresa {nome: 'Construtora Eta'})
        MATCH (e11:Empresa {nome: 'Saude Lambda'})
        CREATE (e7)-[:PARTICIPA_DE {percentual: 40.0, data_entrada: '2022-02-28'}]->(e11)
    """)

    tx.run("""
        MATCH (e8:Empresa {nome: 'Midia Theta'})
        MATCH (e12:Empresa {nome: 'Educacao Mu'})
        CREATE (e8)-[:PARTICIPA_DE {percentual: 65.0, data_entrada: '2022-09-01'}]->(e12)
    """)

    # Cross-ownership (adds complexity)
    tx.run("""
        MATCH (e2:Empresa {nome: 'Tech Beta'})
        MATCH (e9:Empresa {nome: 'Agro Iota'})
        CREATE (e2)-[:PARTICIPA_DE {percentual: 20.0, data_entrada: '2021-06-30'}]->(e9)
    """)

What does this function do?

This function uses Cypher CREATE statements to build the entire ownership structure in three stages:

  • First, it creates 3 Pessoa nodes (Carlos Silva, Maria Santos, Joao Oliveira) and 12 Empresa nodes representing companies of different industries.
  • Then it uses MATCH to find existing nodes and CREATE to add PARTICIPA_DE relationships between them with ownership percentages.
  • The structure has 3 levels of depth. The holding company owns subsidiaries, which in turn own smaller companies. A cross-ownership link between Tech Beta and Agro Iota adds realistic complexity.

Finally, add the main function that runs everything and prints a verification summary.

  • Below the create_ownership_structure function, paste the main function and entry point:
def main():
    with driver.session() as session:
        print("Limpando banco de dados...")
        session.execute_write(clear_database)
        print("Criando estrutura societaria...")
        session.execute_write(create_ownership_structure)
        print("Estrutura societaria criada com sucesso!")
        
        # Verify
        result = session.run("MATCH (n) RETURN count(n) AS total_nodes")
        record = result.single()
        print(f"Total de nos criados: {record['total_nodes']}")
        
        result = session.run("MATCH ()-[r]->() RETURN count(r) AS total_rels")
        record = result.single()
        print(f"Total de relacionamentos criados: {record['total_rels']}")

    driver.close()


if __name__ == "__main__":
    main()

What does the main function do?

The main function opens a session, clears any existing data, then creates the ownership structure. After creation, it runs two count queries to confirm how many nodes and relationships were inserted.

The execute_write method ensures each function runs inside a write transaction, which Neo4j requires for any data-modifying operations.

  • Save populate_graph.py.

✔️ Awesome, I've got everything!

Great. Double check that you have saved the file with your actual credentials filled in.

ⓧ I'd like to double check the full code

from neo4j import GraphDatabase
import os

NEO4J_URI = "neo4j+s://your-instance-id.databases.neo4j.io"
NEO4J_USERNAME = "neo4j"
NEO4J_PASSWORD = "your-password-here"

driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))


def clear_database(tx):
    tx.run("MATCH (n) DETACH DELETE n")


def create_ownership_structure(tx):
    # Create Pessoa nodes (individuals)
    tx.run("""
        CREATE (p1:Pessoa {nome: 'Carlos Silva', cpf: '111.222.333-44'})
        CREATE (p2:Pessoa {nome: 'Maria Santos', cpf: '555.666.777-88'})
        CREATE (p3:Pessoa {nome: 'Joao Oliveira', cpf: '999.000.111-22'})
    """)

    # Create Empresa nodes (companies)
    tx.run("""
        CREATE (e1:Empresa {nome: 'Holding Alpha', cnpj: '11.111.111/0001-01', tipo: 'Holding'})
        CREATE (e2:Empresa {nome: 'Tech Beta', cnpj: '22.222.222/0001-02', tipo: 'Tecnologia'})
        CREATE (e3:Empresa {nome: 'Comercio Gamma', cnpj: '33.333.333/0001-03', tipo: 'Comercio'})
        CREATE (e4:Empresa {nome: 'Servicos Delta', cnpj: '44.444.444/0001-04', tipo: 'Servicos'})
        CREATE (e5:Empresa {nome: 'Logistica Epsilon', cnpj: '55.555.555/0001-05', tipo: 'Logistica'})
        CREATE (e6:Empresa {nome: 'Financeira Zeta', cnpj: '66.666.666/0001-06', tipo: 'Financeira'})
        CREATE (e7:Empresa {nome: 'Construtora Eta', cnpj: '77.777.777/0001-07', tipo: 'Construcao'})
        CREATE (e8:Empresa {nome: 'Midia Theta', cnpj: '88.888.888/0001-08', tipo: 'Midia'})
        CREATE (e9:Empresa {nome: 'Agro Iota', cnpj: '99.999.999/0001-09', tipo: 'Agronegocio'})
        CREATE (e10:Empresa {nome: 'Energia Kappa', cnpj: '10.101.010/0001-10', tipo: 'Energia'})
        CREATE (e11:Empresa {nome: 'Saude Lambda', cnpj: '12.121.212/0001-11', tipo: 'Saude'})
        CREATE (e12:Empresa {nome: 'Educacao Mu', cnpj: '13.131.313/0001-12', tipo: 'Educacao'})
    """)

    # Create ownership relationships (Pessoa -> Empresa)
    tx.run("""
        MATCH (p1:Pessoa {nome: 'Carlos Silva'})
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        CREATE (p1)-[:PARTICIPA_DE {percentual: 60.0, data_entrada: '2015-03-01'}]->(e1)
    """)

    tx.run("""
        MATCH (p2:Pessoa {nome: 'Maria Santos'})
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        CREATE (p2)-[:PARTICIPA_DE {percentual: 40.0, data_entrada: '2015-03-01'}]->(e1)
    """)

    tx.run("""
        MATCH (p3:Pessoa {nome: 'Joao Oliveira'})
        MATCH (e3:Empresa {nome: 'Comercio Gamma'})
        CREATE (p3)-[:PARTICIPA_DE {percentual: 25.0, data_entrada: '2020-06-15'}]->(e3)
    """)

    # Create ownership relationships (Empresa -> Empresa) - Level 1
    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e2:Empresa {nome: 'Tech Beta'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 80.0, data_entrada: '2016-01-10'}]->(e2)
    """)

    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e3:Empresa {nome: 'Comercio Gamma'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 75.0, data_entrada: '2016-05-20'}]->(e3)
    """)

    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e4:Empresa {nome: 'Servicos Delta'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 100.0, data_entrada: '2017-02-01'}]->(e4)
    """)

    tx.run("""
        MATCH (e1:Empresa {nome: 'Holding Alpha'})
        MATCH (e6:Empresa {nome: 'Financeira Zeta'})
        CREATE (e1)-[:PARTICIPA_DE {percentual: 55.0, data_entrada: '2018-08-12'}]->(e6)
    """)

    # Create ownership relationships (Empresa -> Empresa) - Level 2
    tx.run("""
        MATCH (e2:Empresa {nome: 'Tech Beta'})
        MATCH (e5:Empresa {nome: 'Logistica Epsilon'})
        CREATE (e2)-[:PARTICIPA_DE {percentual: 51.0, data_entrada: '2019-03-15'}]->(e5)
    """)

    tx.run("""
        MATCH (e3:Empresa {nome: 'Comercio Gamma'})
        MATCH (e7:Empresa {nome: 'Construtora Eta'})
        CREATE (e3)-[:PARTICIPA_DE {percentual: 60.0, data_entrada: '2019-07-01'}]->(e7)
    """)

    tx.run("""
        MATCH (e4:Empresa {nome: 'Servicos Delta'})
        MATCH (e8:Empresa {nome: 'Midia Theta'})
        CREATE (e4)-[:PARTICIPA_DE {percentual: 70.0, data_entrada: '2020-01-10'}]->(e8)
    """)

    tx.run("""
        MATCH (e6:Empresa {nome: 'Financeira Zeta'})
        MATCH (e9:Empresa {nome: 'Agro Iota'})
        CREATE (e6)-[:PARTICIPA_DE {percentual: 45.0, data_entrada: '2020-11-05'}]->(e9)
    """)

    # Create ownership relationships (Empresa -> Empresa) - Level 3
    tx.run("""
        MATCH (e5:Empresa {nome: 'Logistica Epsilon'})
        MATCH (e10:Empresa {nome: 'Energia Kappa'})
        CREATE (e5)-[:PARTICIPA_DE {percentual: 90.0, data_entrada: '2021-04-20'}]->(e10)
    """)

    tx.run("""
        MATCH (e7:Empresa {nome: 'Construtora Eta'})
        MATCH (e11:Empresa {nome: 'Saude Lambda'})
        CREATE (e7)-[:PARTICIPA_DE {percentual: 40.0, data_entrada: '2022-02-28'}]->(e11)
    """)

    tx.run("""
        MATCH (e8:Empresa {nome: 'Midia Theta'})
        MATCH (e12:Empresa {nome: 'Educacao Mu'})
        CREATE (e8)-[:PARTICIPA_DE {percentual: 65.0, data_entrada: '2022-09-01'}]->(e12)
    """)

    # Cross-ownership (adds complexity)
    tx.run("""
        MATCH (e2:Empresa {nome: 'Tech Beta'})
        MATCH (e9:Empresa {nome: 'Agro Iota'})
        CREATE (e2)-[:PARTICIPA_DE {percentual: 20.0, data_entrada: '2021-06-30'}]->(e9)
    """)


def main():
    with driver.session() as session:
        print("Limpando banco de dados...")
        session.execute_write(clear_database)
        print("Criando estrutura societaria...")
        session.execute_write(create_ownership_structure)
        print("Estrutura societaria criada com sucesso!")
        
        # Verify
        result = session.run("MATCH (n) RETURN count(n) AS total_nodes")
        record = result.single()
        print(f"Total de nos criados: {record['total_nodes']}")
        
        result = session.run("MATCH ()-[r]->() RETURN count(r) AS total_rels")
        record = result.single()
        print(f"Total de relacionamentos criados: {record['total_rels']}")

    driver.close()


if __name__ == "__main__":
    main()
Run the population script

With the script saved and your credentials in place, you can now populate the database.

  • Run the script by executing this command in your terminal:
python populate_graph.py
  • You should see output confirming the creation:
Limpando banco de dados...
Criando estrutura societaria...
Estrutura societaria criada com sucesso!
Total de nos criados: 15
Total de relacionamentos criados: 15

Seeing a connection error?

  • Check that your NEO4J_URI starts with neo4j+s:// and matches the URI in your credentials file exactly.
  • Confirm your instance status is green (Running) in the Aura console. If it shows Paused, click your instance and resume it.
  • Double check that your password has no trailing spaces or missing characters.

Still stuck? Help me debug my Neo4j connection error in populate_graph.py

Verify the graph structure in Neo4j Query

The terminal output confirms counts, but you can also see the graph visually in the Aura console.

  • Switch back to the Aura console in your browser.
  • Click the Connect button on your instance card.
  • Select Query from the dropdown menu.
  • In the Cypher editor at the top, paste this query and press the play button:
MATCH (n)-[r]->(m) RETURN n, r, m

What does this query do?

This Cypher query matches all nodes (n and m) connected by any relationship (r) and returns them. Neo4j Query renders the results as an interactive graph visualization where you can drag nodes around and explore the structure.

  • You should see a network of 15 nodes connected by relationships. Company nodes and person nodes appear connected by PARTICIPA_DE arrows.

Not seeing any results?

  • Make sure your instance shows a green Connected status in the connection bar at the top of the Query tool.
  • If prompted for credentials, enter neo4j as the username and the password from your credentials file.
  • Try running MATCH (n) RETURN count(n) first to confirm the connection works.

Need help? Help me troubleshoot why Neo4j Query is not showing my graph data

Your ownership graph is now populated and verified. Next up, you will write Cypher queries that traverse this multi-level structure to answer real questions about ownership chains and beneficial owners.

Write Cypher Queries for Ownership Traversal

Your database is populated with a realistic corporate ownership structure. Now comes the powerful part: querying it to answer real business questions.

Cypher is Neo4j's query language, purpose-built for traversing relationships. In this step, you'll write queries that follow ownership chains across multiple levels to answer questions like "who ultimately controls this company?"

In this step, get ready to:
  • Query direct shareholders of a specific company.
  • Traverse multi-level ownership chains using variable-length paths.
  • Identify the ultimate beneficial owners in the entire graph.
Query direct shareholders

The most basic ownership question is: who directly owns a given company? In Cypher, you answer this by matching the pattern of nodes and relationships in your graph.

  • Return to Neo4j Browser (the query interface you opened from the Aura console in the previous step).
  • Find all direct shareholders of Comercio Gamma by pasting this query into the editor and clicking the Run button (or pressing Ctrl+Enter):
MATCH (p)-[r:PARTICIPA_DE]->(e:Empresa {nome: 'Comercio Gamma'})
RETURN p, r.percentual

What does this query do?

  • MATCH (p)-[r:PARTICIPA_DE]->(e:Empresa {nome: 'Comercio Gamma'}) finds any node p that has a PARTICIPA_DE relationship pointing to the Empresa node named Comercio Gamma.
  • RETURN p, r.percentual returns the shareholder node and their ownership percentage.

You should see three results: Holding Alpha (75%), Joao Oliveira (25%), representing all direct shareholders of Comercio Gamma.

Now try the reverse. Find all companies that a specific person participates in by running this query:

  • Clear the editor and run this query to find all of Carlos Silva's direct participations:
MATCH (p:Pessoa {nome: 'Carlos Silva'})-[r:PARTICIPA_DE]->(e)
RETURN e.nome, r.percentual

What changed in this pattern?

This time you start from the Pessoa node and follow PARTICIPA_DE outward to find all companies. The arrow direction -> matters in Cypher. It matches the direction the relationship was created.

You should see one result: Holding Alpha at 60%. Carlos only directly owns the holding company. His indirect control over subsidiaries is hidden at this level.

Getting zero results?

  • Check that you're using single quotes around names, not double quotes. Cypher uses single quotes for string literals.
  • Verify the name matches exactly. Run the earlier count query to confirm your data is still there.
  • Help me debug my Cypher query that returns no results.
Traverse multi-level ownership chains

Direct queries only show one level of ownership. The real power of a graph database is following relationships across multiple hops without writing complex recursive joins.

Why graph databases beat SQL here

In a relational database, finding all indirect owners of a company requires recursive CTEs that grow more complex with each level. In Neo4j, you just add an asterisk to the relationship pattern. The database engine traverses each hop in constant time.

  • Find all indirect owners of Energia Kappa (a Level 3 company) by running this variable-length path query:
MATCH path = (p)-[:PARTICIPA_DE*]->(e:Empresa {nome: 'Energia Kappa'})
RETURN p.nome, length(path) AS depth
ORDER BY depth

What does this query do?

  • [:PARTICIPA_DE*] is a variable-length path. The asterisk tells Neo4j to follow one or more hops of PARTICIPA_DE relationships.
  • length(path) calculates how many hops separate each owner from Energia Kappa. A depth of 1 means direct owner, 2 means one intermediary, and so on.

You should see results at multiple depths. Logistica Epsilon is at depth 1 (direct owner), Tech Beta at depth 2, Holding Alpha at depth 3, and the individuals Carlos Silva and Maria Santos at depth 4.

Now for the most important business question: who is the ultimate beneficial owner? This is the entity at the very top of the chain with no one above them.

  • Identify all ultimate beneficial owners in the graph by running:
MATCH (root)
WHERE NOT ()-[:PARTICIPA_DE]->(root)
AND (root)-[:PARTICIPA_DE]->()
RETURN root.nome, labels(root)

What does this query do?

  • WHERE NOT ()-[:PARTICIPA_DE]->(root) filters for nodes that have no incoming ownership. Nobody owns them.
  • The second condition (root)-[:PARTICIPA_DE]->() ensures the node actually owns something. This excludes leaf companies that are just owned but don't own others.

You should see Carlos Silva, Maria Santos, and Joao Oliveira. These three individuals sit at the top of the ownership pyramid with no entity above them.

Seeing companies in your results too?

  • If you omit the second condition, leaf companies (like Energia Kappa) also appear because nothing points to them either. The second line ensures you only get nodes that actually own something.
  • Help me understand why my beneficial owner query returns unexpected results.
Run advanced ownership queries

With the basics of variable-length paths mastered, you can now answer more complex business questions about control and depth.

  • Find all entities controlled by Carlos Silva (direct and indirect) by running:
MATCH (p:Pessoa {nome: 'Carlos Silva'})-[:PARTICIPA_DE*]->(e)
RETURN e.nome, labels(e)[0] AS tipo

What does this query do?

This follows every ownership path outward from Carlos Silva, regardless of how many companies sit between him and the final entity. You should see Holding Alpha and every subsidiary it controls, all the way down to Level 3 companies like Energia Kappa and Educacao Mu.

You should see a large list of companies. Carlos controls the entire Holding Alpha tree through his 60% ownership at the top.

  • Detect the deepest ownership chain in the entire graph by running:
MATCH path = (start)-[:PARTICIPA_DE*]->(end)
WHERE NOT (end)-[:PARTICIPA_DE]->()
WITH path, length(path) AS depth
ORDER BY depth DESC
LIMIT 1
RETURN path

What does this query do?

  • WHERE NOT (end)-[:PARTICIPA_DE]->() ensures the path ends at a leaf node (a company that doesn't own anything else).
  • ORDER BY depth DESC LIMIT 1 picks only the longest path. This reveals the maximum depth of your ownership structure.

Neo4j Browser should render this as a graph visualization showing the full chain. The deepest path has 4 hops: from Carlos Silva or Maria Santos all the way down to Energia Kappa (through Holding Alpha, Tech Beta, and Logistica Epsilon).

Query taking too long?

  • Variable-length paths without bounds can be expensive on large graphs. For this project's 15 nodes, it runs instantly. On production data, add a max depth like [:PARTICIPA_DE*..10] to limit traversal.
  • My variable-length Cypher query is slow or timing out.

You now have a full toolkit of Cypher queries for ownership analysis. Next up, you'll bring these queries to life in an interactive web visualization where you can explore the graph visually.

Build the Interactive Visualization

Your graph database is populated and your Cypher queries work. Now it is time to turn that data into something you can see and interact with.

In this step, you will build an HTML page that connects directly to your Neo4j AuraDB instance and renders the ownership graph as a draggable, color-coded network using neovis.js.

In this step, get ready to:
  • Create the HTML page with neovis.js and configure the graph visualization.
  • Add interactive query buttons that switch between different ownership views.
  • Test the visualization in your browser.
Create the HTML page with neovis.js

The neovis.js library wraps the vis.js visualization engine and the Neo4j JavaScript driver into one package. It connects directly from the browser to your AuraDB instance and renders Cypher query results as interactive graphs.

  • Switch back to index.html in your editor.
  • Paste the following HTML structure that includes the neovis.js CDN, page styles, and the page layout:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Visualizador de Cadeia Societaria</title>
    <script src="https://unpkg.com/neovis.js@2.0.2"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background-color: #1a1a2e;
            color: #e0e0e0;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }

        header {
            background-color: #16213e;
            padding: 16px 24px;
            border-bottom: 1px solid #0f3460;
        }

        header h1 {
            font-size: 1.4rem;
            color: #4fc3f7;
        }

        .controls {
            display: flex;
            gap: 12px;
            padding: 12px 24px;
            background-color: #16213e;
            border-bottom: 1px solid #0f3460;
            flex-wrap: wrap;
        }

        .controls button {
            padding: 8px 16px;
            border: 1px solid #0f3460;
            border-radius: 6px;
            background-color: #1a1a2e;
            color: #4fc3f7;
            cursor: pointer;
            font-size: 0.85rem;
            transition: background-color 0.2s;
        }

        .controls button:hover {
            background-color: #0f3460;
        }

        .controls button.active {
            background-color: #0f3460;
            border-color: #4fc3f7;
        }

        #viz {
            flex: 1;
            width: 100%;
        }

        .info-panel {
            position: fixed;
            bottom: 16px;
            right: 16px;
            background-color: #16213e;
            border: 1px solid #0f3460;
            border-radius: 8px;
            padding: 16px;
            max-width: 300px;
            font-size: 0.85rem;
            display: none;
        }

        .info-panel h3 {
            color: #4fc3f7;
            margin-bottom: 8px;
        }

        .info-panel p {
            margin-bottom: 4px;
            color: #b0b0b0;
        }
    </style>
</head>
<body>
    <header>
        <h1>Visualizador de Cadeia Societaria</h1>
    </header>

    <div class="controls">
        <button id="btn-full" class="active" onclick="showFullGraph()">Grafo Completo</button>
        <button id="btn-holding" onclick="showHoldingChain()">Cadeia da Holding Alpha</button>
        <button id="btn-owners" onclick="showBeneficialOwners()">Beneficiarios Finais</button>
        <button id="btn-depth" onclick="showDeepestChain()">Cadeia Mais Profunda</button>
    </div>

    <div id="viz"></div>

    <div class="info-panel" id="info-panel">
        <h3 id="info-title">Detalhes</h3>
        <p id="info-content"></p>
    </div>

What does this code do?

  • The <script src="https://unpkg.com/neovis.js@2.0.2"> tag loads neovis.js from the CDN. This single library includes both the Neo4j driver and vis.js for rendering.
  • The CSS sets up a dark theme with a full-height layout. The #viz div fills the remaining screen space where the graph will render.
  • Four buttons in the .controls bar will trigger different Cypher queries to change what the graph shows.
  • The .info-panel is a hidden panel that can display node details on hover.

Now add the JavaScript section that configures the NeoVis connection and node styling. Paste this directly below the closing </div> of the info-panel:

    <script>
        const NEO4J_URL = "neo4j+s://your-instance-id.databases.neo4j.io";
        const NEO4J_USER = "neo4j";
        const NEO4J_PASSWORD = "your-password-here";

        let neoViz;

        function initViz(cypher) {
            const config = {
                containerId: "viz",
                neo4j: {
                    serverUrl: NEO4J_URL,
                    serverUser: NEO4J_USER,
                    serverPassword: NEO4J_PASSWORD,
                },
                labels: {
                    Empresa: {
                        label: "nome",
                        [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
                            static: {
                                color: "#4fc3f7",
                                shape: "box",
                                font: { color: "#ffffff", size: 12 }
                            }
                        }
                    },
                    Pessoa: {
                        label: "nome",
                        [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
                            static: {
                                color: "#ff7043",
                                shape: "ellipse",
                                font: { color: "#ffffff", size: 12 }
                            }
                        }
                    }
                },
                relationships: {
                    PARTICIPA_DE: {
                        value: "percentual",
                        [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
                            function: {
                                title: function (edge) {
                                    return edge.properties.percentual + "%";
                                },
                                label: function (edge) {
                                    return edge.properties.percentual + "%";
                                }
                            },
                            static: {
                                arrows: { to: { enabled: true } },
                                color: { color: "#66bb6a", highlight: "#a5d6a7" },
                                font: { color: "#a5d6a7", size: 10, strokeWidth: 0 }
                            }
                        }
                    }
                },
                visConfig: {
                    nodes: {
                        borderWidth: 2,
                        shadow: true
                    },
                    edges: {
                        smooth: { type: "cubicBezier" }
                    },
                    physics: {
                        hierarchicalRepulsion: {
                            centralGravity: 0.0,
                            springLength: 150,
                            springConstant: 0.01
                        }
                    },
                    layout: {
                        hierarchical: {
                            enabled: true,
                            direction: "UD",
                            sortMethod: "directed",
                            levelSeparation: 120
                        }
                    }
                },
                initialCypher: cypher
            };

            neoViz = new NeoVis.default(config);
            neoViz.render();
        }

What does the NeoVis config do?

The config object tells neovis.js how to connect to your database and how to style the graph:

  • The neo4j block holds your AuraDB connection URI, username, and password.
  • The labels section maps each node label to a visual style. Empresa nodes appear as blue boxes. Pessoa nodes appear as orange ellipses.
  • The relationships section styles PARTICIPA_DE edges with green arrows and ownership percentage labels.
  • The visConfig section sets a hierarchical top-down layout so ownership flows visually from top to bottom.
  • Replace your-instance-id.databases.neo4j.io in the NEO4J_URL constant with the connection URI from your AuraDB credentials file.
  • Replace your-password-here in the NEO4J_PASSWORD constant with the password you saved when creating your instance.

Find your credentials

Open the credentials text file you downloaded when creating your AuraDB instance. It contains the connection URI and password. The username is always neo4j.

Add interactive query buttons

Each button in the controls bar needs a function that runs a different Cypher query and re-renders the graph. The renderWithCypher method on the NeoVis instance lets you swap the displayed graph without reinitializing the entire visualization.

  • Paste the following query functions directly below the closing } of the initViz function:
        function setActiveButton(id) {
            document.querySelectorAll('.controls button').forEach(function(btn) {
                btn.classList.remove('active');
            });
            document.getElementById(id).classList.add('active');
        }

        function showFullGraph() {
            setActiveButton('btn-full');
            const query = "MATCH (n)-[r:PARTICIPA_DE]->(m) RETURN n, r, m";
            if (neoViz) {
                neoViz.renderWithCypher(query);
            } else {
                initViz(query);
            }
        }

        function showHoldingChain() {
            setActiveButton('btn-holding');
            const query = "MATCH path = (e:Empresa {nome: 'Holding Alpha'})-[:PARTICIPA_DE*]->(sub) RETURN path";
            neoViz.renderWithCypher(query);
        }

        function showBeneficialOwners() {
            setActiveButton('btn-owners');
            const query = "MATCH (p:Pessoa)-[r:PARTICIPA_DE]->(e:Empresa) WHERE NOT ()-[:PARTICIPA_DE]->(p) RETURN p, r, e";
            neoViz.renderWithCypher(query);
        }

        function showDeepestChain() {
            setActiveButton('btn-depth');
            const query = "MATCH path = (start)-[:PARTICIPA_DE*]->(end) WHERE NOT (end)-[:PARTICIPA_DE]->() WITH path, length(path) AS depth ORDER BY depth DESC LIMIT 1 RETURN path";
            neoViz.renderWithCypher(query);
        }

        // Initialize on page load
        document.addEventListener("DOMContentLoaded", function() {
            showFullGraph();
        });
    </script>
</body>
</html>

What do these functions do?

  • The setActiveButton helper highlights whichever button was clicked so the learner knows which view is active.
  • The showFullGraph function shows all nodes and relationships. On first call it initializes the visualization. On later calls it just re-renders.
  • The showHoldingChain function uses a variable-length path pattern [:PARTICIPA_DE*] to trace all subsidiaries under Holding Alpha.
  • The showBeneficialOwners function finds people (Pessoa nodes) who have no incoming PARTICIPA_DE relationships. These are the top-level individuals who ultimately own everything.
  • The showDeepestChain function finds the longest ownership chain by ordering paths by depth and returning the single longest one.
  • Save index.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file with the correct connection credentials filled in.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Visualizador de Cadeia Societaria</title>
    <script src="https://unpkg.com/neovis.js@2.0.2"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background-color: #1a1a2e;
            color: #e0e0e0;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }

        header {
            background-color: #16213e;
            padding: 16px 24px;
            border-bottom: 1px solid #0f3460;
        }

        header h1 {
            font-size: 1.4rem;
            color: #4fc3f7;
        }

        .controls {
            display: flex;
            gap: 12px;
            padding: 12px 24px;
            background-color: #16213e;
            border-bottom: 1px solid #0f3460;
            flex-wrap: wrap;
        }

        .controls button {
            padding: 8px 16px;
            border: 1px solid #0f3460;
            border-radius: 6px;
            background-color: #1a1a2e;
            color: #4fc3f7;
            cursor: pointer;
            font-size: 0.85rem;
            transition: background-color 0.2s;
        }

        .controls button:hover {
            background-color: #0f3460;
        }

        .controls button.active {
            background-color: #0f3460;
            border-color: #4fc3f7;
        }

        #viz {
            flex: 1;
            width: 100%;
        }

        .info-panel {
            position: fixed;
            bottom: 16px;
            right: 16px;
            background-color: #16213e;
            border: 1px solid #0f3460;
            border-radius: 8px;
            padding: 16px;
            max-width: 300px;
            font-size: 0.85rem;
            display: none;
        }

        .info-panel h3 {
            color: #4fc3f7;
            margin-bottom: 8px;
        }

        .info-panel p {
            margin-bottom: 4px;
            color: #b0b0b0;
        }
    </style>
</head>
<body>
    <header>
        <h1>Visualizador de Cadeia Societaria</h1>
    </header>

    <div class="controls">
        <button id="btn-full" class="active" onclick="showFullGraph()">Grafo Completo</button>
        <button id="btn-holding" onclick="showHoldingChain()">Cadeia da Holding Alpha</button>
        <button id="btn-owners" onclick="showBeneficialOwners()">Beneficiarios Finais</button>
        <button id="btn-depth" onclick="showDeepestChain()">Cadeia Mais Profunda</button>
    </div>

    <div id="viz"></div>

    <div class="info-panel" id="info-panel">
        <h3 id="info-title">Detalhes</h3>
        <p id="info-content"></p>
    </div>

    <script>
        const NEO4J_URL = "neo4j+s://your-instance-id.databases.neo4j.io";
        const NEO4J_USER = "neo4j";
        const NEO4J_PASSWORD = "your-password-here";

        let neoViz;

        function initViz(cypher) {
            const config = {
                containerId: "viz",
                neo4j: {
                    serverUrl: NEO4J_URL,
                    serverUser: NEO4J_USER,
                    serverPassword: NEO4J_PASSWORD,
                },
                labels: {
                    Empresa: {
                        label: "nome",
                        [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
                            static: {
                                color: "#4fc3f7",
                                shape: "box",
                                font: { color: "#ffffff", size: 12 }
                            }
                        }
                    },
                    Pessoa: {
                        label: "nome",
                        [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
                            static: {
                                color: "#ff7043",
                                shape: "ellipse",
                                font: { color: "#ffffff", size: 12 }
                            }
                        }
                    }
                },
                relationships: {
                    PARTICIPA_DE: {
                        value: "percentual",
                        [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
                            function: {
                                title: function (edge) {
                                    return edge.properties.percentual + "%";
                                },
                                label: function (edge) {
                                    return edge.properties.percentual + "%";
                                }
                            },
                            static: {
                                arrows: { to: { enabled: true } },
                                color: { color: "#66bb6a", highlight: "#a5d6a7" },
                                font: { color: "#a5d6a7", size: 10, strokeWidth: 0 }
                            }
                        }
                    }
                },
                visConfig: {
                    nodes: {
                        borderWidth: 2,
                        shadow: true
                    },
                    edges: {
                        smooth: { type: "cubicBezier" }
                    },
                    physics: {
                        hierarchicalRepulsion: {
                            centralGravity: 0.0,
                            springLength: 150,
                            springConstant: 0.01
                        }
                    },
                    layout: {
                        hierarchical: {
                            enabled: true,
                            direction: "UD",
                            sortMethod: "directed",
                            levelSeparation: 120
                        }
                    }
                },
                initialCypher: cypher
            };

            neoViz = new NeoVis.default(config);
            neoViz.render();
        }

        function setActiveButton(id) {
            document.querySelectorAll('.controls button').forEach(function(btn) {
                btn.classList.remove('active');
            });
            document.getElementById(id).classList.add('active');
        }

        function showFullGraph() {
            setActiveButton('btn-full');
            const query = "MATCH (n)-[r:PARTICIPA_DE]->(m) RETURN n, r, m";
            if (neoViz) {
                neoViz.renderWithCypher(query);
            } else {
                initViz(query);
            }
        }

        function showHoldingChain() {
            setActiveButton('btn-holding');
            const query = "MATCH path = (e:Empresa {nome: 'Holding Alpha'})-[:PARTICIPA_DE*]->(sub) RETURN path";
            neoViz.renderWithCypher(query);
        }

        function showBeneficialOwners() {
            setActiveButton('btn-owners');
            const query = "MATCH (p:Pessoa)-[r:PARTICIPA_DE]->(e:Empresa) WHERE NOT ()-[:PARTICIPA_DE]->(p) RETURN p, r, e";
            neoViz.renderWithCypher(query);
        }

        function showDeepestChain() {
            setActiveButton('btn-depth');
            const query = "MATCH path = (start)-[:PARTICIPA_DE*]->(end) WHERE NOT (end)-[:PARTICIPA_DE]->() WITH path, length(path) AS depth ORDER BY depth DESC LIMIT 1 RETURN path";
            neoViz.renderWithCypher(query);
        }

        // Initialize on page load
        document.addEventListener("DOMContentLoaded", function() {
            showFullGraph();
        });
    </script>
</body>
</html>
Test the visualization

The page is ready to connect to your database and render the graph live in your browser.

  • Open index.html in your browser by double-clicking the file in Finder (macOS) or File Explorer (Windows).

After a few seconds, the graph should render. You should see blue rectangular nodes for companies, orange elliptical nodes for people, and green arrows between them with percentage labels.

  • Try dragging a node. The graph is interactive and nodes reposition as you move them.
  • Click the Cadeia da Holding Alpha button. The view should filter to only show Holding Alpha and its subsidiaries.
  • Click Beneficiarios Finais. This shows only the people at the top of the ownership chain connected to their direct companies.
  • Click Cadeia Mais Profunda. This highlights the single longest ownership path in the entire graph.
  • Click Grafo Completo to return to the full view.

Graph not rendering?

  • Open the browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows) and check for errors.
  • If you see a WebSocket or encryption error, make sure the NEO4J_URL starts with neo4j+s:// followed by your actual instance ID. Copy the connection URI exactly as it appears in your credentials file.
  • If you see a password-related error, confirm you pasted the password from the credentials file without leading or trailing spaces.
  • Make sure your AuraDB instance shows green "Running" status in the Aura console. If it is paused, click Resume and wait for it to start.

Still stuck? Help me debug why my neovis.js graph is not connecting to AuraDB.

Your ownership graph is now alive in the browser. You can visually trace who owns what, filter by specific views, and explore the multi-level corporate structure interactively. In the Secret Mission, you will add click-to-trace functionality that highlights the path from any entity to its ultimate beneficial owner.

Secret mission

Highlight the Beneficial Owner Path

Your graph visualizer shows the full ownership structure, but can you make it interactive? In this challenge, you'll add click-to-trace functionality. When a user clicks any node, the app traces the ownership path all the way up to the ultimate beneficial owner and displays it in a panel.

Clean Up Your Resources

Clean Up Your Resources

This project runs on Neo4j AuraDB Free, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Neo4j AuraDB Free instance (auto-pauses after 72 hours of inactivity, deleted after 30 days if paused).
  • Local project files (populate_graph.py and index.html).
  • Python virtual environment with the neo4j driver installed.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your ownership graph.

  • Your AuraDB Free instance is free forever. It auto-pauses after 3 days without write queries, but you can resume it with one click from the Aura console.
  • Your local files stay on disk and take up minimal space.
  • Your Python virtual environment remains available for future scripts.

Pause - I'll come back to this later

AuraDB Free pauses automatically after 72 hours of no write queries. There is nothing you need to do manually to pause it.

  • Your instance will auto-pause on its own. You can resume it anytime from the Aura console within 30 days.
  • If a paused instance is not resumed within 30 days, Neo4j deletes it permanently.
  • Your local files and virtual environment stay on disk until you choose to remove them.

Delete - I don't want to use this again

Remove all project resources and start fresh. This is permanent.

  • Delete your Neo4j AuraDB Free instance:
  • Go to console.neo4j.io and sign in.
  • On your instance card, click the trashcan icon (or the three-dot menu and select Delete).
  • Type the exact name of your instance to confirm, then click Destroy.
  • This permanently removes all graph data. There is no way to recover it.

Delete your local project files by running these commands:

macOS

cd ~/Desktop
rm -rf populate_graph.py index.html

Windows

cd ~\Desktop
Remove-Item populate_graph.py, index.html -Force
  • If you created a virtual environment folder (e.g. venv), delete that folder as well from your project directory.
  • The neo4j Python package was installed inside the virtual environment. Deleting the virtual environment folder removes it.

Nice Work!

Nice Work!

You just built a full corporate ownership visualizer powered by a graph database. From modeling complex multi-level ownership chains to rendering them as an interactive, explorable network, you now have hands-on experience with one of the most powerful tools for relationship-heavy data.

You've learned how to:

  • Model a corporate ownership structure as a property graph with Empresa nodes, Pessoa nodes, and PARTICIPA_DE relationships in a Neo4j AuraDB Free instance.
  • Write Cypher queries for multi-level ownership traversal, depth analysis, and ultimate beneficial owner identification.
  • Build an interactive web visualization with neovis.js that renders color-coded nodes, directional edges, and ownership percentage labels with dynamic query switching.
  • Secret Mission: Added click-to-trace functionality that highlights the ownership path from any entity to its ultimate beneficial owner.

Ready to quiz yourself?